diff --git a/apps/condo/domains/miniapp/access/SendVoIPStartMessageService.js b/apps/condo/domains/miniapp/access/SendVoIPStartMessageService.js new file mode 100644 index 00000000000..940780522f7 --- /dev/null +++ b/apps/condo/domains/miniapp/access/SendVoIPStartMessageService.js @@ -0,0 +1,22 @@ +const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter') + +const { canExecuteServiceAsB2CAppServiceUser } = require('@condo/domains/miniapp/utils/b2cAppServiceUserAccess/server.utils') +const { SERVICE } = require('@condo/domains/user/constants/common') + +async function canSendVoIPStartMessage (args) { + const { authentication: { item: user } } = args + + if (!user) return throwAuthenticationError() + if (user.deletedAt) return false + if (user.isAdmin) return true + + if (user.type === SERVICE) { + return await canExecuteServiceAsB2CAppServiceUser(args) + } + + return false +} + +module.exports = { + canSendVoIPStartMessage, +} \ No newline at end of file diff --git a/apps/condo/domains/miniapp/constants.js b/apps/condo/domains/miniapp/constants.js index df8a9043bf1..03a0a9bbf72 100644 --- a/apps/condo/domains/miniapp/constants.js +++ b/apps/condo/domains/miniapp/constants.js @@ -85,6 +85,7 @@ const ACCESS_TOKEN_WRONG_RIGHT_SET_TYPE = 'ACCESS_TOKEN_WRONG_RIGHT_SET_TYPE' const ACCESS_TOKEN_CONTEXT_DOES_NOT_MATCH_RIGHT_SET = 'ACCESS_TOKEN_CONTEXT_DOES_NOT_MATCH_RIGHT_SET' const ACCESS_TOKEN_SERVICE_USER_DOES_NOT_MATCH_CONTEXT = 'ACCESS_TOKEN_SERVICE_USER_DOES_NOT_MATCH_CONTEXT' const ACCESS_TOKEN_NEGATIVE_TTL = 'ACCESS_TOKEN_NEGATIVE_TTL' +const CALL_DATA_NOT_PROVIDED_ERROR = 'CALL_DATA_NOT_PROVIDED' const MAX_PERMISSION_NAME_LENGTH = 50 const MIN_PERMISSION_NAME_LENGTH = 1 @@ -132,6 +133,9 @@ const ACCESS_TOKEN_SESSION_ID_PREFIX = `${Buffer.from('b2bAccessToken').toString const ACCESS_TOKEN_UPDATE_MANY_CHUNK_SIZE = 500 +const NATIVE_VOIP_TYPE = 'sip' +const B2C_APP_VOIP_TYPE = 'b2cApp' + module.exports = { ALL_APPS_CATEGORY, CONNECTED_APPS_CATEGORY, @@ -207,4 +211,9 @@ module.exports = { DEFAULT_NOTIFICATION_WINDOW_MAX_COUNT, DEFAULT_NOTIFICATION_WINDOW_DURATION_IN_SECONDS, + + CALL_DATA_NOT_PROVIDED_ERROR, + + NATIVE_VOIP_TYPE, + B2C_APP_VOIP_TYPE, } \ No newline at end of file diff --git a/apps/condo/domains/miniapp/gql.js b/apps/condo/domains/miniapp/gql.js index 00aefcfd902..a9215783d34 100644 --- a/apps/condo/domains/miniapp/gql.js +++ b/apps/condo/domains/miniapp/gql.js @@ -29,6 +29,12 @@ const SEND_B2C_APP_PUSH_MESSAGE_MUTATION = gql` } ` +const SEND_VOIP_START_MESSAGE_MUTATION = gql` + mutation sendVoIPStartMessage ($data: SendVoIPStartMessageInput!) { + result: sendVoIPStartMessage(data: $data) { verifiedContactsCount createdMessagesCount erroredMessagesCount } + } +` + const B2B_APP_NEWS_SHARING_CONFIG_FIELDS = `{ publishUrl previewUrl pushNotificationSettings customFormUrl getRecipientsUrl getRecipientsCountersUrl icon { publicUrl } previewPicture { publicUrl } name ${COMMON_FIELDS} }` const B2BAppNewsSharingConfig = generateGqlQueries('B2BAppNewsSharingConfig', B2B_APP_NEWS_SHARING_CONFIG_FIELDS) @@ -135,5 +141,6 @@ module.exports = { B2BAppRoleWithoutEmployeeRole, B2BAppPosIntegrationConfig, B2CAppAccessRightSet, + SEND_VOIP_START_MESSAGE_MUTATION, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/miniapp/schema/B2CAppAccessRightSet.test.js b/apps/condo/domains/miniapp/schema/B2CAppAccessRightSet.test.js index 31d200aaa53..3a091279abc 100644 --- a/apps/condo/domains/miniapp/schema/B2CAppAccessRightSet.test.js +++ b/apps/condo/domains/miniapp/schema/B2CAppAccessRightSet.test.js @@ -4,7 +4,7 @@ const { faker } = require('@faker-js/faker') -const { makeLoggedInAdminClient, makeClient, UUID_RE } = require('@open-condo/keystone/test.utils') +const { makeLoggedInAdminClient, makeClient, UUID_RE, expectToThrowAccessDeniedErrorToResult } = require('@open-condo/keystone/test.utils') const { expectToThrowAuthenticationErrorToObj, expectToThrowAuthenticationErrorToObjects, expectToThrowAccessDeniedErrorToObj, expectToThrowAccessDeniedErrorToObjects, @@ -12,12 +12,14 @@ const { -const { B2CAppAccessRightSet, createTestB2CAppAccessRightSet, updateTestB2CAppAccessRightSet, createTestB2CAppProperty, updateTestB2CAppProperty, B2CAppProperty } = require('@condo/domains/miniapp/utils/testSchema') +const { B2CAppAccessRightSet, createTestB2CAppAccessRightSet, updateTestB2CAppAccessRightSet, createTestB2CAppProperty, updateTestB2CAppProperty, B2CAppProperty, sendVoIPStartMessageByTestClient, updateTestB2CAppAccessRight } = require('@condo/domains/miniapp/utils/testSchema') const { createTestB2CApp, createTestB2CAppAccessRight } = require('@condo/domains/miniapp/utils/testSchema') +const { FLAT_UNIT_TYPE } = require('@condo/domains/property/constants/common') const { buildFakeAddressAndMeta } = require('@condo/domains/property/utils/testSchema/factories') const { makeClientWithNewRegisteredAndLoggedInUser, makeClientWithSupportUser } = require('@condo/domains/user/utils/testSchema') const { makeClientWithServiceUser } = require('@condo/domains/user/utils/testSchema') + describe('B2CAppAccessRightSet', () => { let admin @@ -304,13 +306,14 @@ describe('B2CAppAccessRightSet', () => { let user let b2cApp let serviceUser + let b2cAccessRight beforeEach(async () => { support = await makeClientWithSupportUser() user = await makeClientWithNewRegisteredAndLoggedInUser() serviceUser = await makeClientWithServiceUser(); - [b2cApp] = await createTestB2CApp(admin) - await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp) + [b2cApp] = await createTestB2CApp(admin); + [b2cAccessRight] = await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp) }) test('B2CAppAccessRightSet', async () => { @@ -418,6 +421,57 @@ describe('B2CAppAccessRightSet', () => { expect(foundB2CAppProperty.id).toEqual(createdB2CAppProperty.id) }) + + test('SendVoIPStartMessageService', async () => { + const args = { + app: { id: b2cApp.id }, + addressKey: faker.datatype.uuid(), + unitName: faker.random.alphaNumeric(8), + unitType: FLAT_UNIT_TYPE, + callData: { + callId: faker.random.alphaNumeric(8), + b2cAppCallData: { + B2CAppContext: faker.random.alphaNumeric(8), + }, + }, + } + + // Can't without access right set + await expectToThrowAccessDeniedErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(serviceUser, args) + }) + + // Can't with access right set and without b2c app property + const [rightSet] = await createTestB2CAppAccessRightSet(support, b2cApp, { canExecuteSendVoIPStartMessage: true }) + await updateTestB2CAppAccessRight(support, b2cAccessRight.id, { accessRightSet: { connect: { id: rightSet.id } } }) + await expectToThrowAccessDeniedErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(serviceUser, args) + }) + + // Can with access right set and b2c app property + const [b2cAppProperty] = await createTestB2CAppProperty(serviceUser, b2cApp) + args.addressKey = b2cAppProperty.addressKey + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, args) + expect(result).toEqual(expect.objectContaining({ + verifiedContactsCount: expect.any(Number), + createdMessagesCount: expect.any(Number), + erroredMessagesCount: expect.any(Number), + })) + + // Can't with access right and another b2c app proeprty + const [anotherB2CApp] = await createTestB2CApp(admin) + const [anotherB2CAppProperty] = await createTestB2CAppProperty(admin, anotherB2CApp) + args.addressKey = anotherB2CAppProperty.addressKey + await expectToThrowAccessDeniedErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(serviceUser, args) + }) + + // Can't for another app + args.app.id = anotherB2CApp.id + await expectToThrowAccessDeniedErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(serviceUser, args) + }) + }) }) }) diff --git a/apps/condo/domains/miniapp/schema/SendVoIPStartMessageService.js b/apps/condo/domains/miniapp/schema/SendVoIPStartMessageService.js new file mode 100644 index 00000000000..0e38e8067bf --- /dev/null +++ b/apps/condo/domains/miniapp/schema/SendVoIPStartMessageService.js @@ -0,0 +1,572 @@ +const get = require('lodash/get') +const omit = require('lodash/omit') + +const { GQLError, GQLErrorCode: { BAD_USER_INPUT } } = require('@open-condo/keystone/errors') +const { getLogger } = require('@open-condo/keystone/logging') +const { checkDvAndSender } = require('@open-condo/keystone/plugins/dvAndSender') +const { GQLCustomSchema, find, getByCondition } = require('@open-condo/keystone/schema') + +const { COMMON_ERRORS } = require('@condo/domains/common/constants/errors') +const access = require('@condo/domains/miniapp/access/SendVoIPStartMessageService') +const { + PROPERTY_NOT_FOUND_ERROR, + APP_NOT_FOUND_ERROR, + DEFAULT_NOTIFICATION_WINDOW_MAX_COUNT, + DEFAULT_NOTIFICATION_WINDOW_DURATION_IN_SECONDS, + CALL_DATA_NOT_PROVIDED_ERROR, + NATIVE_VOIP_TYPE, + B2C_APP_VOIP_TYPE, +} = require('@condo/domains/miniapp/constants') +const { B2CAppProperty, CustomValue } = require('@condo/domains/miniapp/utils/serverSchema') +const { setCallStatus, CALL_STATUS_START_SENT } = require('@condo/domains/miniapp/utils/voip') +const { + MESSAGE_META, + VOIP_INCOMING_CALL_MESSAGE_TYPE, MESSAGE_SENDING_STATUS, +} = require('@condo/domains/notification/constants/constants') +const { sendMessage } = require('@condo/domains/notification/utils/serverSchema') +const { UNIT_TYPES } = require('@condo/domains/property/constants/common') +const { getOldestNonDeletedProperty } = require('@condo/domains/property/utils/serverSchema/helpers') +const { Resident } = require('@condo/domains/resident/utils/serverSchema') +const { RedisGuard } = require('@condo/domains/user/utils/serverSchema/guards') + +const CACHE_TTL = { + DEFAULT: DEFAULT_NOTIFICATION_WINDOW_DURATION_IN_SECONDS, + [VOIP_INCOMING_CALL_MESSAGE_TYPE]: 2, +} + +const POSSIBLE_CUSTOM_FIELD_NAMES = Object.keys(get(MESSAGE_META[VOIP_INCOMING_CALL_MESSAGE_TYPE], 'data', {})).filter(key => key.startsWith('voip')) +const redisGuard = new RedisGuard() + +const logger = getLogger() + +const SERVICE_NAME = 'sendVoIPStartMessage' +const ERRORS = { + PROPERTY_NOT_FOUND: { + mutation: SERVICE_NAME, + variable: ['data', 'addressKey'], + code: BAD_USER_INPUT, + type: PROPERTY_NOT_FOUND_ERROR, + message: 'Unable to find Property or B2CAppProperty by provided addressKey', + messageForUser: `api.miniapp.${SERVICE_NAME}.PROPERTY_NOT_FOUND`, + }, + APP_NOT_FOUND: { + mutation: SERVICE_NAME, + variable: ['data', 'addressKey'], + code: BAD_USER_INPUT, + type: APP_NOT_FOUND_ERROR, + message: 'Unable to find B2CApp.', + messageForUser: `api.miniapp.${SERVICE_NAME}.APP_NOT_FOUND`, + }, + CALL_DATA_NOT_PROVIDED: { + mutation: SERVICE_NAME, + variable: ['data, callData'], + type: CALL_DATA_NOT_PROVIDED_ERROR, + code: BAD_USER_INPUT, + message: '"b2cAppCallData" or "nativeCallData" or both should be provided', + }, + DV_VERSION_MISMATCH: { + ...COMMON_ERRORS.DV_VERSION_MISMATCH, + mutation: SERVICE_NAME, + }, + WRONG_SENDER_FORMAT: { + ...COMMON_ERRORS.WRONG_SENDER_FORMAT, + mutation: SERVICE_NAME, + }, +} + +function logInfo ({ b2cAppId, callId, stats, err }) { + logger.info({ msg: `${SERVICE_NAME} stats`, entityName: 'B2CApp', entityId: b2cAppId, data: { callId, stats }, err: err }) +} + +async function checkLimits ({ context, b2cAppId, logContext }) { + const appSettings = await getByCondition('AppMessageSetting', { + b2cApp: { id: b2cAppId }, + type: VOIP_INCOMING_CALL_MESSAGE_TYPE, + deletedAt: null, + }) + + const searchKey = `${VOIP_INCOMING_CALL_MESSAGE_TYPE}-${b2cAppId}` + const ttl = CACHE_TTL[VOIP_INCOMING_CALL_MESSAGE_TYPE] || CACHE_TTL['DEFAULT'] + + try { + await redisGuard.checkCustomLimitCounters( + `${SERVICE_NAME}-${searchKey}`, + appSettings?.notificationWindowSize ?? ttl, + appSettings?.numberOfNotificationInWindow ?? DEFAULT_NOTIFICATION_WINDOW_MAX_COUNT, + context, + ) + } catch (err) { + logContext.logInfoStats.step = 'check limits' + logInfo({ ...logContext, err: err }) + throw err + } +} + +function getInitialLogContext ({ addressKey, unitName, unitType, app, callData }) { + return { + logInfoStats: { + step: 'init', + addressKey, + unitName, + unitType, + verifiedContactsCount: 0, + residentsCount: 0, + verifiedResidentsCount: 0, + createdMessagesCount: 0, + erroredMessagesCount: 0, + createMessageErrors: [], + isStatusCached: false, + isPropertyFound: false, + isAppFound: false, + }, + b2cAppId: app.id, + callId: callData.callId, + } +} + +async function sendMessageToUser ({ + context, resident, contact, user, + customVoIPValuesByContactId, + sender, dv, b2cApp: { id: b2cAppId, name: b2cAppName }, + callData, +}) { + const customVoIPValues = customVoIPValuesByContactId[contact.id] || {} + + // NOTE(YEgorLu): as in domains/notification/constants/config for VOIP_INCOMING_CALL_MESSAGE_TYPE + let preparedDataArgs = { + B2CAppId: b2cAppId, + B2CAppName: b2cAppName, + residentId: resident.id, + callId: callData.callId, + } + + const needToPasteB2CAppCallData = !callData.nativeCallData || (callData.b2cAppCallData && customVoIPValues.voipType === B2C_APP_VOIP_TYPE) + + if (!needToPasteB2CAppCallData) { + let voipType = customVoIPValues.voipType || NATIVE_VOIP_TYPE + // CustomValue says to use B2C_APP_VOIP_TYPE, but we have no b2cAppCallData + if (voipType === B2C_APP_VOIP_TYPE) { + voipType = NATIVE_VOIP_TYPE + } + + preparedDataArgs = { + ...preparedDataArgs, + voipType: voipType, + voipAddress: callData.nativeCallData.voipAddress, + voipLogin: callData.nativeCallData.voipLogin, + voipPassword: callData.nativeCallData.voipPassword, + voipDtfmCommand: callData.nativeCallData.voipPanels?.[0]?.dtmfCommand, + voipPanels: callData.nativeCallData.voipPanels, + stunServers: callData.nativeCallData.stunServers, + stun: callData.nativeCallData.stunServers?.[0], + codec: callData.nativeCallData.codec, + ...omit(customVoIPValues, 'voipType'), + } + } else { + preparedDataArgs = { + ...preparedDataArgs, + B2CAppContext: callData.b2cAppCallData.B2CAppContext, + } + } + + const requiredMetaData = get(MESSAGE_META[VOIP_INCOMING_CALL_MESSAGE_TYPE], 'data', {}) + const metaData = Object.fromEntries( + Object.keys(requiredMetaData).map((key) => [key, preparedDataArgs[key]]) + ) + + const messageAttrs = { + sender, + type: VOIP_INCOMING_CALL_MESSAGE_TYPE, + to: { user: { id: user.id } }, + meta: { + dv, + title: '', // NOTE(YEgorLu): title and body are rewritten by translations for push type + body: '', + data: metaData, + }, + } + + const result = await sendMessage(context, messageAttrs) + return { resident, contact, user, result } +} + +async function getVerifiedResidentsWithContacts ({ context, logContext, addressKey, unitName, unitType }) { + let verifiedResidentsWithContacts = [] + + const oldestProperty = await getOldestNonDeletedProperty({ addressKey }) + logContext.logInfoStats.step = 'find property' + logContext.logInfoStats.propertyFound = !!oldestProperty + + if (!oldestProperty?.id) { + logInfo(logContext) + return { + verifiedResidentsWithContacts, + earlyReturnValue: { + verifiedContactsCount: 0, + createdMessagesCount: 0, + erroredMessagesCount: 0, + }, + } + } + + const allVerifiedContactsOnUnit = await find('Contact', { + property: { id: oldestProperty.id, deletedAt: null }, + unitName_i: unitName, + unitType: unitType, + isVerified: true, + deletedAt: null, + }) + logContext.logInfoStats.step = 'find contacts' + logContext.logInfoStats.contactsCount = allVerifiedContactsOnUnit.length + + if (!allVerifiedContactsOnUnit.length) { + logInfo(logContext) + return { + verifiedResidentsWithContacts, + earlyReturnValue: { + verifiedContactsCount: 0, + createdMessagesCount: 0, + erroredMessagesCount: 0, + }, + } + } + + const allResidentsOnUnit = await Resident.getAll(context, { + addressKey, + unitName, + unitType, + deletedAt: null, + }, 'id user { id phone }') + logContext.logInfoStats.step = 'find residents' + logContext.logInfoStats.residentsCount = allResidentsOnUnit.length + + if (!allResidentsOnUnit.length) { + logInfo(logContext) + return { + verifiedResidentsWithContacts, + earlyReturnValue: { + verifiedContactsCount: allVerifiedContactsOnUnit.length, + createdMessagesCount: 0, + erroredMessagesCount: 0, + }, + } + } + + const verifiedResidentsWithContactsByPhone = {} + + for (const contact of allVerifiedContactsOnUnit) { + const verifiedResidentWithContact = { contact, resident: null, user: null } + verifiedResidentsWithContacts.push(verifiedResidentWithContact) + verifiedResidentsWithContactsByPhone[verifiedResidentWithContact.contact.phone] = verifiedResidentWithContact + } + + for (const resident of allResidentsOnUnit) { + const phone = resident.user.phone + if (!phone || !verifiedResidentsWithContactsByPhone[phone]) continue + verifiedResidentsWithContactsByPhone[phone].resident = resident + verifiedResidentsWithContactsByPhone[phone].user = resident.user + } + + verifiedResidentsWithContacts = verifiedResidentsWithContacts.filter(({ resident, user, contact }) => !!resident && !!contact && !!user) + + logContext.logInfoStats.step = 'find verified residents' + logContext.logInfoStats.contactsCount = verifiedResidentsWithContacts.length + + return { + verifiedResidentsWithContacts, + earlyReturnValue: { + verifiedContactsCount: allVerifiedContactsOnUnit.length, + createdMessagesCount: 0, + erroredMessagesCount: 0, + }, + } +} + +async function verifyApp ({ context, logContext, addressKey, app }) { + + const b2cAppId = app.id + + const b2cAppProperty = await B2CAppProperty.getOne(context, { + app: { id: b2cAppId, deletedAt: null }, + addressKey: addressKey, + deletedAt: null, + }, 'id app { id name }') + + if (!b2cAppProperty) { + logInfo(logContext) + throw new GQLError(ERRORS.PROPERTY_NOT_FOUND, context) + } + logContext.logInfoStats.isPropertyFound = true + + if (!b2cAppProperty.app) { + logInfo(logContext) + throw new GQLError(ERRORS.APP_NOT_FOUND, context) + } + logContext.logInfoStats.isAppFound = true + + return { b2cAppId, b2cAppName: b2cAppProperty.app.name } +} + +async function getCustomVoIPValuesByContacts ({ context, contactIds }) { + const customVoIPValues = await CustomValue.getAll(context, { + customField: { name_in: POSSIBLE_CUSTOM_FIELD_NAMES, deletedAt: null }, + itemId_in: contactIds, + deletedAt: null, + }, 'id itemId data customField { name }') + return customVoIPValues.reduce((byContactId, customValue) => { + if (!byContactId[customValue.itemId]) byContactId[customValue.itemId] = {} + byContactId[customValue.itemId][customValue.customField.name] = customValue.data + return byContactId + }, {}) +} + +function parseSendMessageResults ({ logContext, sendMessagePromisesResults }) { + const sendMessageStats = sendMessagePromisesResults.map(promiseResult => { + if (promiseResult.status === 'rejected') { + logContext.logInfoStats.erroredMessagesCount++ + logContext.logInfoStats.createMessageErrors.push(promiseResult.reason) + return { error: promiseResult.reason } + } + const { resident, result } = promiseResult.value + + if (result.isDuplicateMessage) { + logContext.logInfoStats.erroredMessagesCount++ + logContext.logInfoStats.createMessageErrors.push(`${resident.id} duplicate message`) + return { error: `${resident.id} duplicate message` } + } + if (result.status !== MESSAGE_SENDING_STATUS) { + logContext.logInfoStats.erroredMessagesCount++ + logContext.logInfoStats.createMessageErrors.push(`${resident.id} invalid status for some reason`) + return { error: `${resident.id} invalid status for some reason` } + } + logContext.logInfoStats.createdMessagesCount++ + return result + }) + + for (const messageStat of sendMessageStats) { + if (messageStat.error) { + logContext.logInfoStats.erroredMessagesCount++ + logContext.logInfoStats.createMessageErrors.push(messageStat.error) + continue + } + logContext.logInfoStats.createdMessagesCount++ + } + + return sendMessageStats +} + +function parseStartingMessagesIdsByUserIdsByMessageResults ({ sendMessagePromisesResults }) { + const startingMessagesIdsByUserIds = {} + for (const promiseResult of sendMessagePromisesResults) { + if (promiseResult.status !== 'fulfilled') { + continue + } + + const { user, result } = promiseResult.value + if (result?.id) { + startingMessagesIdsByUserIds[user.id] = result.id + } + } + + return startingMessagesIdsByUserIds +} + +const SendVoIPStartMessageService = new GQLCustomSchema('SendVoIPStartMessageService', { + types: [ + { + access: true, + type: `input SendVoIPStartMessageVoIPPanelParameters { + """ + Dtmf command used to open the panel + """ + dtmfCommand: String! + """ + Name of a panel to be displayed + """ + name: String + }`, + }, + { + access: true, + type: `input SendVoIPStartMessageDataForCallHandlingByB2CApp { + """ + Data that will be provided to B2CApp. May be stringified JSON + """ + B2CAppContext: String!, + }`, + }, + { + access: true, + type: `input SendVoIPStartMessageDataForCallHandlingByNative { + """ + Address of sip server, which device should connect to + """ + voipAddress: String!, + """ + Login for connection to sip server + """ + voipLogin: String!, + """ + Password for connection to sip server + """ + voipPassword: String!, + """ + Panels and their commands to open. First one must be the main one. Multiple panels are in testing stage right now and may change + """ + voipPanels: [SendVoIPStartMessageVoIPPanelParameters!]! + """ + Stun server urls. Are used to determine device public ip for media streams + """ + stunServers: [String!], + """ + Preferred codec (usually vp8) + """ + codec: String + }`, + }, + { + access: true, + type: `input SendVoIPStartMessageData { + """ + Unique value for each call session between panel and resident (means same for different devices also). + Must be provided for correct work with multiple devices that use same voip call. + F.e. to cancel calls with CANCELED_CALL_MESSAGE_PUSH messages + """ + callId: String!, + """ + If you want your B2CApp to handle incoming VoIP call, provide this argument. + """ + b2cAppCallData: SendVoIPStartMessageDataForCallHandlingByB2CApp, + """ + If you want mobile app to handle call (without your B2CApp), provide this argument. If "b2cAppCallData" and "nativeCallData" are provided together, native call is prioritized. + """ + nativeCallData: SendVoIPStartMessageDataForCallHandlingByNative + }`, + }, + { + access: true, + type: `input SendVoIPStartMessageInput { + dv: Int!, + sender: SenderFieldInput!, + app: B2CAppWhereUniqueInput!, + """ + Should be "addressKey" of B2CAppProperty / Property for which you want to send message + """ + addressKey: String!, + """ + Name of unit, same as in Property map + """ + unitName: String!, + """ + Type of unit, same as in Property map + """ + unitType: SendVoIPStartMessageUnitType!, + callData: SendVoIPStartMessageData! + }`, + }, + { + access: true, + type: `type SendVoIPStartMessageOutput { + """ + Count of all Organization Contacts, which we possibly could've sent messages to + """ + verifiedContactsCount: Int, + """ + Count of Messages that will be sent, one for each verified Resident + """ + createdMessagesCount: Int, + """ + Count of Messages which was not created due to some internal error + """ + erroredMessagesCount: Int + }`, + }, + { + access: true, + type: `enum SendVoIPStartMessageUnitType { ${UNIT_TYPES.join('\n')} }`, + }, + ], + + mutations: [ + { + access: access.canSendVoIPStartMessage, + schema: 'sendVoIPStartMessage(data: SendVoIPStartMessageInput!): SendVoIPStartMessageOutput', + doc: { + summary: 'Mutation sends VOIP_INCOMING_CALL Messages to each verified resident on address + unit. Also caches calls, so mobile app can properly react to cancel calls. ' + + 'You can either provide all data.* arguments so mobile app will use it\'s own app to answer call, or provide just B2CAppContext + callId to use your B2CApp\'s calling app', + errors: omit(ERRORS, 'DV_VERSION_MISMATCH', 'WRONG_SENDER_FORMAT'), + }, + resolver: async (parent, args, context) => { + const { data: argsData } = args + const { dv, sender, app, addressKey, unitName, unitType, callData } = argsData + + if (!callData.b2cAppCallData && !callData.nativeCallData) { + throw new GQLError(ERRORS.CALL_DATA_NOT_PROVIDED, context) + } + + checkDvAndSender(argsData, ERRORS.DV_VERSION_MISMATCH, ERRORS.WRONG_SENDER_FORMAT, context) + + const logContext = getInitialLogContext(argsData) + + // 1) Check B2CApp and B2CAppProperty + const { b2cAppId, b2cAppName } = await verifyApp({ context, logContext, addressKey, app }) + + // 2) Get verified residents + const { verifiedResidentsWithContacts, earlyReturnValue } = await getVerifiedResidentsWithContacts({ context, logContext, addressKey, unitName, unitType }) + if (!verifiedResidentsWithContacts.length) { + return earlyReturnValue + } + const { verifiedContactsCount } = earlyReturnValue + + // 3) Check limits + await checkLimits({ context, logContext, b2cAppId }) + + const customVoIPValuesByContactId = await getCustomVoIPValuesByContacts({ context, contactIds: [...new Set(verifiedResidentsWithContacts.map(({ contact }) => contact.id))] }) + + // 4) Send messages + /** @type {Array>} */ + const sendMessagePromises = verifiedResidentsWithContacts + .map(({ resident, contact, user }) => { + return sendMessageToUser({ + context, resident, contact, user, customVoIPValuesByContactId, + dv, sender, callData, b2cApp: { id: b2cAppId, name: b2cAppName }, + }) + }) + + // 5) Set call status in redis + logContext.logInfoStats.step = 'send messages' + const sendMessageResults = await Promise.allSettled(sendMessagePromises) + const sendMessageStats = parseSendMessageResults({ sendMessagePromisesResults: sendMessageResults, logContext }) + const startingMessagesIdsByUserIds = parseStartingMessagesIdsByUserIdsByMessageResults({ sendMessagePromisesResults: sendMessageResults }) + + if (sendMessageStats.some(stat => !stat.error)) { + logContext.logInfoStats.isStatusCached = await setCallStatus({ + b2cAppId, + callId: callData.callId, + status: CALL_STATUS_START_SENT, + // NOTE(YEgorLu): we can use uniqKey for that: [pushType, b2cAppId, callId, userId, YYYY-MM-DD].join() + // but this would require to check current and previous day/period + // for now lets save it in session, usually we receive cancel message in less than 1 minute anyway + startingMessagesIdsByUserIds: startingMessagesIdsByUserIds, // check uniqKey + }) + } + + logContext.logInfoStats.step = 'result' + logInfo(logContext) + + return { + verifiedContactsCount, + createdMessagesCount: sendMessageStats.filter(stat => !stat.error).length, + erroredMessagesCount: sendMessageStats.filter(stat => !!stat.error).length, + } + }, + }, + ], + +}) + +module.exports = { + SendVoIPStartMessageService, + ERRORS, + CACHE_TTL, +} diff --git a/apps/condo/domains/miniapp/schema/SendVoIPStartMessageService.test.js b/apps/condo/domains/miniapp/schema/SendVoIPStartMessageService.test.js new file mode 100644 index 00000000000..5da615fc8a5 --- /dev/null +++ b/apps/condo/domains/miniapp/schema/SendVoIPStartMessageService.test.js @@ -0,0 +1,776 @@ +const { faker } = require('@faker-js/faker') + +const { + makeLoggedInAdminClient, + makeClient, + expectToThrowAccessDeniedErrorToResult, + expectToThrowAuthenticationErrorToResult, + expectToThrowGQLErrorToResult, + expectToThrowGraphQLRequestError, +} = require('@open-condo/keystone/test.utils') + +const { createTestContact } = require('@condo/domains/contact/utils/testSchema') +const { + createTestB2CApp, + sendVoIPStartMessageByTestClient, + createTestB2CAppProperty, + createTestB2CAppAccessRight, + createTestB2CAppAccessRightSet, + updateTestB2CAppAccessRight, + createTestB2BApp, + createTestB2BAppContext, + createTestB2BAppAccessRightSet, + createTestB2BAppAccessRight, + createTestCustomValue, + createTestAppMessageSetting, + createTestCustomField, +} = require('@condo/domains/miniapp/utils/testSchema') +const { getCallStatus, CALL_STATUS_START_SENT } = require('@condo/domains/miniapp/utils/voip') +const { + VOIP_INCOMING_CALL_MESSAGE_TYPE, +} = require('@condo/domains/notification/constants/constants') +const { Message } = require('@condo/domains/notification/utils/testSchema') +const { FLAT_UNIT_TYPE } = require('@condo/domains/property/constants/common') +const { makeClientWithResidentAccessAndProperty } = require('@condo/domains/property/utils/testSchema') +const { createTestResident } = require('@condo/domains/resident/utils/testSchema') +const { makeClientWithSupportUser, makeClientWithNewRegisteredAndLoggedInUser, + makeClientWithResidentUser, createTestPhone, + makeClientWithServiceUser, +} = require('@condo/domains/user/utils/testSchema') + +const { ERRORS } = require('./SendVoIPStartMessageService') + +const { NATIVE_VOIP_TYPE, B2C_APP_VOIP_TYPE } = require('../constants') + +async function prepareSingleActor ({ admin, organization, property, unitName, unitType }) { + const phone = createTestPhone() + const userClient = await makeClientWithResidentUser({}, { phone }) + const [contact] = await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: unitType, + isVerified: true, + phone: phone, + }) + const [resident] = await createTestResident(admin, userClient.user, property, { + unitName: unitName, + unitType: unitType, + }) + + return { + user: userClient.user, + contact, + resident, + } +} + +describe('SendVoIPStartMessageService', () => { + let admin + + beforeAll(async () => { + admin = await makeLoggedInAdminClient() + }) + + describe('Access', () => { + let b2cApp + let property + let serviceUser + let b2cAccessRight + + beforeAll(async () => { + const [testB2CApp] = await createTestB2CApp(admin) + b2cApp = testB2CApp + + const { property: testProperty } = await makeClientWithResidentAccessAndProperty() + property = testProperty + await createTestB2CAppProperty(admin, b2cApp, { address: testProperty.address, addressMeta: testProperty.addressMeta }) + serviceUser = await makeClientWithServiceUser(); + [b2cAccessRight] = await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp) + }) + + const TEST_CASES = [ + { name: 'admin can', getClient: () => admin, expectError: null }, + { name: 'support can\'t', getClient: () => makeClientWithSupportUser(), expectError: expectToThrowAccessDeniedErrorToResult }, + { name: 'user can\'t', getClient: () => makeClientWithNewRegisteredAndLoggedInUser(), expectError: expectToThrowAccessDeniedErrorToResult }, + { name: 'anonymous can\'t', getClient: () => makeClient(), expectError: expectToThrowAuthenticationErrorToResult }, + { + name: 'service user without access right set and b2c app property can\'t', + getClient: () => serviceUser, + expectError: expectToThrowAccessDeniedErrorToResult, + }, + { + name: 'service user with access right set and b2c app property can', + getClient: async () => { + const [rightSet] = await createTestB2CAppAccessRightSet(admin, b2cApp, { canExecuteSendVoIPStartMessage: true }) + await updateTestB2CAppAccessRight(admin, b2cAccessRight.id, { accessRightSet: { connect: { id: rightSet.id } } }) + return serviceUser + }, + expectError: null, + }, + ] + + test.each(TEST_CASES)('$name', async ({ getClient, expectError }) => { + const client = await getClient() + const mutate = () => sendVoIPStartMessageByTestClient(client, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: faker.random.alphaNumeric(3), + unitType: FLAT_UNIT_TYPE, + callData: { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + if (expectError) { + await expectError(async () => await mutate()) + } else { + const [result] = await mutate() + expect(result).toBeDefined() + expect(result.verifiedContactsCount).toBe(0) + expect(result.createdMessagesCount).toBe(0) + expect(result.erroredMessagesCount).toBe(0) + } + }) + }) + + describe('Validation', () => { + + test('should throw error if no app with provided id', async () => { + await expectToThrowGQLErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(admin, { + app: { id: faker.datatype.uuid() }, + addressKey: faker.datatype.uuid(), + unitName: faker.random.alphaNumeric(3), + unitType: FLAT_UNIT_TYPE, + callData: { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + }, ERRORS.PROPERTY_NOT_FOUND) + }) + + test('should throw error if no b2cappproperty with provided addressKey', async () => { + const [b2cApp] = await createTestB2CApp(admin) + + await expectToThrowGQLErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(admin, { + app: { id: b2cApp.id }, + addressKey: faker.datatype.uuid(), + unitName: faker.random.alphaNumeric(3), + unitType: FLAT_UNIT_TYPE, + callData: { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + }, ERRORS.PROPERTY_NOT_FOUND) + }) + + test('should throw error if callData does not contain data for b2c or native call', async () => { + const [b2cApp] = await createTestB2CApp(admin) + const { property } = await makeClientWithResidentAccessAndProperty() + await createTestB2CAppProperty(admin, b2cApp, { address: property.address, addressMeta: property.addressMeta }) + + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + await expectToThrowGQLErrorToResult(async () => { + await sendVoIPStartMessageByTestClient(admin, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: { + callId: faker.datatype.uuid(), + }, + }) + }, ERRORS.CALL_DATA_NOT_PROVIDED) + }) + + }) + + describe('Logic', () => { + let serviceUser + let b2cAppProperty + let organization + let property + let b2cApp + + beforeAll(async () => { + const [testB2CApp] = await createTestB2CApp(admin) + b2cApp = testB2CApp + + const { organization: testOrganization, property: testProperty } = await makeClientWithResidentAccessAndProperty() + organization = testOrganization + property = testProperty; + [b2cAppProperty] = await createTestB2CAppProperty(admin, b2cApp, { address: testProperty.address, addressMeta: testProperty.addressMeta }) + serviceUser = await makeClientWithServiceUser() + const [accessRightSet] = await createTestB2CAppAccessRightSet(admin, b2cApp, { canExecuteSendVoIPStartMessage: true }) + await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp, { accessRightSet: { connect: { id: accessRightSet.id } } }) + + await createTestAppMessageSetting(admin, { b2cApp, numberOfNotificationInWindow: 1000, type: VOIP_INCOMING_CALL_MESSAGE_TYPE }) + }) + + test('successfully sends VoIP start message when all conditions are met', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + const verifiedContactsCount = 5 + const verifiedResidentsCount = 3 + const notVerifiedResidentsCount = verifiedContactsCount - verifiedResidentsCount + + const prepareDataPromises = [] + + for (let i = 0; i < verifiedContactsCount; i++) { + prepareDataPromises.push((async (admin, organization, property) => { + const phone = createTestPhone() + const userClient = await makeClientWithResidentUser({}, { phone }) + await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: unitType, + isVerified: true, + phone: phone, + }) + const needToCreateVerifiedResident = i < verifiedResidentsCount + if (needToCreateVerifiedResident) { + await createTestResident(admin, userClient.user, property, { + unitName: unitName, + unitType: unitType, + }) + } + })(admin, organization, property)) + } + for (let i = 0; i < notVerifiedResidentsCount; i++) { + prepareDataPromises.push((async (admin, property) => { + const userClient = await makeClientWithResidentUser() + await createTestResident(admin, userClient.user, property, { + unitName: unitName, + unitType: unitType, + }) + })(admin, property)) + } + await Promise.all(prepareDataPromises) + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + + expect(result.verifiedContactsCount).toBe(verifiedContactsCount) + expect(result.createdMessagesCount).toBe(verifiedResidentsCount) + expect(result.erroredMessagesCount).toBe(0) + }) + + test('returns zeroish stats when no verified contacts found on unit', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: unitType, + isVerified: false, + }) + const userClient = await makeClientWithResidentUser() + await createTestResident(admin, userClient.user, property, { + unitName: unitName, + unitType: unitType, + }) + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + + expect(result.verifiedContactsCount).toBe(0) + expect(result.createdMessagesCount).toBe(0) + expect(result.erroredMessagesCount).toBe(0) + }) + + test('returns zero message attempts when no residents found for verified contacts', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: unitType, + isVerified: true, + }) + const userClient = await makeClientWithResidentUser() + await createTestResident(admin, userClient.user, property, { + unitName: faker.random.alphaNumeric(3), + unitType: unitType, + }) + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + + expect(result.verifiedContactsCount).toBe(1) + expect(result.createdMessagesCount).toBe(0) + expect(result.erroredMessagesCount).toBe(0) + }) + + describe('Cache', () => { + test('Saves call status in cache', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + const residentsCount = 3 + const prepareDataPromises = [] + + for (let i = 0; i < residentsCount; i++) { + prepareDataPromises.push((async (admin) => { + const phone = createTestPhone() + const userClient = await makeClientWithResidentUser({}, { phone }) + await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: unitType, + isVerified: true, + phone: phone, + }) + await createTestResident(admin, userClient.user, property, { + unitName: unitName, + unitType: unitType, + }) + })(admin)) + } + await Promise.all(prepareDataPromises) + + const callId = faker.datatype.uuid() + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: { callId, b2cAppCallData: { B2CAppContext: '' } }, + }) + expect(result.verifiedContactsCount).toBe(residentsCount) + expect(result.createdMessagesCount).toBe(residentsCount) + expect(result.erroredMessagesCount).toBe(0) + + const cache = await getCallStatus({ b2cAppId: b2cApp.id, callId }) + expect(cache).not.toBe(null) + expect(cache.status).toBe(CALL_STATUS_START_SENT) + }) + + test('Saves User.id to Message.id binding', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + const residentsCount = 3 + const prepareDataPromises = [] + const userIds = [] + + for (let i = 0; i < residentsCount; i++) { + prepareDataPromises.push((async (admin) => { + const phone = createTestPhone() + const userClient = await makeClientWithResidentUser({}, { phone }) + userIds.push(userClient.user.id) + await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: unitType, + isVerified: true, + phone: phone, + }) + await createTestResident(admin, userClient.user, property, { + unitName: unitName, + unitType: unitType, + }) + })(admin)) + } + await Promise.all(prepareDataPromises) + + const callId = faker.datatype.uuid() + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: { callId, b2cAppCallData: { B2CAppContext: '' } }, + }) + expect(result.verifiedContactsCount).toBe(residentsCount) + expect(result.createdMessagesCount).toBe(residentsCount) + expect(result.erroredMessagesCount).toBe(0) + + const createdMessages = await Message.getAll(admin, { + user: { id_in: userIds }, + type: VOIP_INCOMING_CALL_MESSAGE_TYPE, + }, { first: residentsCount }) + expect(createdMessages).toHaveLength(residentsCount) + const expectedUserIdToMessageId = Object.fromEntries(createdMessages.map(message => ([message.user.id, message.id]))) + + const cache = await getCallStatus({ b2cAppId: b2cApp.id, callId }) + expect(cache).not.toBe(null) + expect(cache.startingMessagesIdsByUserIds).toEqual(expectedUserIdToMessageId) + }) + + test('Does not save status if have no users to send push', async () => { + const unitName = faker.random.alphaNumeric(3) + await createTestContact(admin, organization, property, { + unitName: unitName, + unitType: FLAT_UNIT_TYPE, + isVerified: true, + }) + const callId = faker.datatype.uuid() + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: FLAT_UNIT_TYPE, + callData: { + callId: callId, + b2cAppCallData: { B2CAppContext: '' }, + }, + }) + expect(result.verifiedContactsCount).toBe(1) + expect(result.createdMessagesCount).toBe(0) + expect(result.erroredMessagesCount).toBe(0) + + const cache = await getCallStatus({ b2cAppId: b2cApp.id, callId }) + expect(cache).toBe(null) + }) + }) + + describe('voipType priority', () => { + let customVoIPTypeField + + beforeAll(async () => { + [customVoIPTypeField] = await createTestCustomField(admin, { + name: 'voipType', + modelName: 'Contact', + type: 'String', + validationRules: null, + isVisible: false, + priority: 0, + isUniquePerObject: true, + staffCanRead: false, + sender: { dv: 1, fingerprint: faker.random.alphaNumeric(8) }, + }) + }) + + test('complex case with everything', async () => { + const serviceUser = await makeClientWithServiceUser() + const [b2cApp] = await createTestB2CApp(admin) + const { organization, property } = await makeClientWithResidentAccessAndProperty() + await createTestB2CAppProperty(admin, b2cApp, { address: property.address, addressMeta: property.addressMeta }) + const [accessRightSet] = await createTestB2CAppAccessRightSet(admin, b2cApp, { canExecuteSendVoIPStartMessage: true }) + await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp, { accessRightSet: { connect: { id: accessRightSet.id } } }) + + + const [b2bApp] = await createTestB2BApp(admin) + await createTestB2BAppContext(admin, b2bApp, organization, { status: 'Finished' }) + const [b2bAppAccessRightSet] = await createTestB2BAppAccessRightSet(admin, b2bApp, { canReadOrganizations: true, canReadCustomValues: true, canManageCustomValues: true }) + await createTestB2BAppAccessRight(admin, serviceUser.user, b2bApp, b2bAppAccessRightSet) + + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + + const residentsCount = 4 + const prepareDataPromises = [] + const actors = [] + + for (let i = 0; i < residentsCount; i++) { + prepareDataPromises.push((async ({ admin, organization, property, unitName, unitType }) => { + const actor = await prepareSingleActor({ admin, organization, property, unitName, unitType }) + actors.push(actor) + })({ admin, organization, property, unitName, unitType })) + } + await Promise.all(prepareDataPromises) + + const normalVoIPType = 'sip' + const resident1VoIPType = 'variant2' + const resident2VoIPType = 'variant3' + const resident3VoIPType = B2C_APP_VOIP_TYPE + + const customVoIPTypesWithActors = [ + [resident1VoIPType, actors[0]], + [resident2VoIPType, actors[1]], + [resident3VoIPType, actors[2]], + ] + + for (const [customVoIPType, actor] of customVoIPTypesWithActors) { + await createTestCustomValue(serviceUser, customVoIPTypeField, organization, { + sourceType: 'B2BApp', + data: customVoIPType, + itemId: actor.contact.id, + sourceId: b2bApp.id, + isUniquePerObject: true, + }) + } + + const dataAttrs = { + callId: faker.datatype.uuid(), + b2cAppCallData: { + B2CAppContext: faker.random.alphaNumeric(10), + }, + nativeCallData: { + voipAddress: faker.internet.ip(), + voipLogin: faker.internet.userName(), + voipPassword: faker.internet.password(), + voipPanels: [ + { dtmfCommand: faker.random.alphaNumeric(2), name: faker.random.alphaNumeric(3) }, + { dtmfCommand: faker.random.alphaNumeric(2), name: faker.random.alphaNumeric(3) }, + ], + stunServers: [faker.internet.ip(), faker.internet.ip()], + codec: 'vp8', + }, + } + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: unitType, + callData: dataAttrs, + }) + expect(result.verifiedContactsCount).toBe(residentsCount) + expect(result.createdMessagesCount).toBe(residentsCount) + expect(result.erroredMessagesCount).toBe(0) + + const createdMessages = await Message.getAll(admin, { + user: { id_in: actors.map(actor => actor.user.id) }, + type: VOIP_INCOMING_CALL_MESSAGE_TYPE, + }, { first: residentsCount }) + + expect(createdMessages).toHaveLength(residentsCount) + expect([...new Set(createdMessages.map(msg => msg.user))]).toHaveLength(residentsCount) + + const createdMessagesByResident = createdMessages.reduce((byResident, msg) => { + byResident[msg.meta.data.residentId] = msg + return byResident + }, {}) + expect(Object.keys(createdMessagesByResident)).toEqual(expect.arrayContaining(actors.map(actor => actor.resident.id))) + + + const defaultMetaDataFields = { + B2CAppId: b2cApp.id, + B2CAppName: b2cApp.name, + callId: dataAttrs.callId, + } + const defaultMetaDataB2CAppCallDataFields = { + ...defaultMetaDataFields, + B2CAppContext: dataAttrs.b2cAppCallData.B2CAppContext, + } + const defaultMetaDataNativeCallDataFields = { + ...defaultMetaDataFields, + voipAddress: dataAttrs.nativeCallData.voipAddress, + voipLogin: dataAttrs.nativeCallData.voipLogin, + voipPassword: dataAttrs.nativeCallData.voipPassword, + voipDtfmCommand: dataAttrs.nativeCallData.voipPanels[0].dtmfCommand, + voipPanels: dataAttrs.nativeCallData.voipPanels, + stun: dataAttrs.nativeCallData.stunServers[0], + stunServers: dataAttrs.nativeCallData.stunServers, + codec: dataAttrs.nativeCallData.codec, + } + + const normalVoIPTypeMessages = createdMessages.filter(msg => msg.meta.data.voipType === normalVoIPType) + try { + expect(normalVoIPTypeMessages).toHaveLength(1) + } catch (err) { + console.error('normalVoIPTypeMessages fail') + throw err + } + expect(normalVoIPTypeMessages[0]).toEqual(expect.objectContaining({ + meta: expect.objectContaining({ + data: expect.objectContaining({ + ...defaultMetaDataNativeCallDataFields, + residentId: actors[3].resident.id, + voipType: String(normalVoIPType), + }), + }), + })) + + const resident1VoIPTypeMessages = createdMessages.filter(msg => msg.meta.data.voipType === resident1VoIPType) + try { + expect(resident1VoIPTypeMessages).toHaveLength(1) + } catch (err) { + console.error('resident1VoIPTypeMessages fail') + throw err + } + expect(resident1VoIPTypeMessages[0]).toEqual(expect.objectContaining({ + meta: expect.objectContaining({ + data: expect.objectContaining({ + ...defaultMetaDataNativeCallDataFields, + residentId: actors[0].resident.id, + voipType: String(resident1VoIPType), + }), + }), + })) + + const resident2VoIPTypeMessages = createdMessages.filter(msg => msg.meta.data.voipType === resident2VoIPType) + try { + expect(resident2VoIPTypeMessages).toHaveLength(1) + } catch (err) { + console.error('resident2VoIPTypeMessages fail') + throw err + } + expect(resident2VoIPTypeMessages[0]).toEqual(expect.objectContaining({ + meta: expect.objectContaining({ + data: expect.objectContaining({ + ...defaultMetaDataNativeCallDataFields, + residentId: actors[1].resident.id, + voipType: String(resident2VoIPType), + }), + }), + })) + + const b2cAppVoIPTypeMessages = createdMessages.filter(msg => !!msg.meta.data.B2CAppContext) + expect(b2cAppVoIPTypeMessages).toHaveLength(1) + try { + expect(b2cAppVoIPTypeMessages).toHaveLength(1) + } catch (err) { + console.error('b2cAppVoIPTypeMessages fail') + throw err + } + expect(b2cAppVoIPTypeMessages[0]).toEqual(expect.objectContaining({ + meta: expect.objectContaining({ + data: expect.objectContaining({ + ...defaultMetaDataB2CAppCallDataFields, + residentId: actors[2].resident.id, + }), + }), + })) + + }) + + test('uses native call voipType when CustomValue is absent', async () => { + const unitName = faker.random.alphaNumeric(3) + const unitType = FLAT_UNIT_TYPE + const actor = await prepareSingleActor({ admin, organization, property, unitName, unitType }) + + const callData = { + callId: faker.datatype.uuid(), + b2cAppCallData: { B2CAppContext: faker.random.alphaNumeric(10) }, + nativeCallData: { + voipAddress: faker.internet.ip(), + voipLogin: faker.internet.userName(), + voipPassword: faker.internet.password(), + voipPanels: [{ dtmfCommand: faker.random.alphaNumeric(2), name: faker.random.alphaNumeric(3) }], + stunServers: [faker.internet.ip()], + codec: 'vp8', + }, + } + + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName, + unitType, + callData, + }) + + expect(result.verifiedContactsCount).toBe(1) + expect(result.createdMessagesCount).toBe(1) + expect(result.erroredMessagesCount).toBe(0) + + const [createdMessage] = await Message.getAll(admin, { + user: { id: actor.user.id }, + type: VOIP_INCOMING_CALL_MESSAGE_TYPE, + }, { first: 1 }) + + expect(createdMessage).toEqual(expect.objectContaining({ + meta: expect.objectContaining({ + data: expect.objectContaining({ + residentId: actor.resident.id, + voipType: NATIVE_VOIP_TYPE, + voipAddress: callData.nativeCallData.voipAddress, + }), + }), + })) + expect(createdMessage.meta.data.B2CAppContext).toBeUndefined() + }) + + }) + + test('applies values from CustomFields with names equals to native voip push keys starting with "voip"', async () => { + const [b2cApp] = await createTestB2CApp(admin) + const { organization, property } = await makeClientWithResidentAccessAndProperty() + await createTestB2CAppProperty(admin, b2cApp, { address: property.address, addressMeta: property.addressMeta }) + const unitName = faker.random.alphaNumeric(3) + + const serviceUser = await makeClientWithServiceUser() + const [b2bApp] = await createTestB2BApp(admin) + await createTestB2BAppContext(admin, b2bApp, organization, { status: 'Finished' }) + const [b2bAppAccessRightSet] = await createTestB2BAppAccessRightSet(admin, b2bApp, { canReadOrganizations: true, canReadCustomValues: true, canManageCustomValues: true }) + await createTestB2BAppAccessRight(admin, serviceUser.user, b2bApp, b2bAppAccessRightSet) + + const [b2cRightSet] = await createTestB2CAppAccessRightSet(admin, b2cApp, { canExecuteSendVoIPStartMessage: true }) + await createTestB2CAppAccessRight(admin, serviceUser.user, b2cApp, { accessRightSet: { connect: { id: b2cRightSet.id } } }) + + + const actor = await prepareSingleActor({ admin, organization, property, unitName, unitType: FLAT_UNIT_TYPE }) + + + const customValue = faker.random.alphaNumeric(8) + const customFieldNames = ['voipAddress', 'voipPassword', 'voipDtfmCommand', 'voipPanels'] + for (const fieldName of customFieldNames) { + const [customField] = await createTestCustomField(admin, { + name: fieldName, + modelName: 'Contact', + type: 'String', + validationRules: null, + isVisible: false, + priority: 0, + isUniquePerObject: true, + staffCanRead: false, + sender: { dv: 1, fingerprint: faker.random.alphaNumeric(8) }, + }) + await createTestCustomValue(serviceUser, customField, organization, { + sourceType: 'B2BApp', + data: customValue, + itemId: actor.contact.id, + sourceId: b2bApp.id, + isUniquePerObject: true, + }) + } + const callId = faker.datatype.uuid() + const [result] = await sendVoIPStartMessageByTestClient(serviceUser, { + app: { id: b2cApp.id }, + addressKey: property.addressKey, + unitName: unitName, + unitType: FLAT_UNIT_TYPE, + callData: { + callId: callId, + nativeCallData: { + voipAddress: faker.internet.ip(), + voipLogin: faker.internet.userName(), + voipPassword: faker.internet.password(), + voipPanels: [{ dtmfCommand: faker.random.alphaNumeric(2), name: faker.random.alphaNumeric(3) }], + stunServers: [faker.internet.ip()], + codec: 'vp8', + }, + }, + }) + expect(result.verifiedContactsCount).toBe(1) + expect(result.createdMessagesCount).toBe(1) + expect(result.erroredMessagesCount).toBe(0) + + const [createdMessage] = await Message.getAll(admin, { type: VOIP_INCOMING_CALL_MESSAGE_TYPE, user: { id: actor.user.id } }) + + expect(createdMessage.meta.data).toEqual(expect.objectContaining( + Object.fromEntries(customFieldNames.map(fieldName => ([fieldName, customValue]))) + )) + }) + }) +}) \ No newline at end of file diff --git a/apps/condo/domains/miniapp/schema/index.js b/apps/condo/domains/miniapp/schema/index.js index 59d58f54c02..aec0230adbc 100644 --- a/apps/condo/domains/miniapp/schema/index.js +++ b/apps/condo/domains/miniapp/schema/index.js @@ -25,6 +25,8 @@ const { CustomValue } = require('./CustomValue') const { MessageAppBlackList } = require('./MessageAppBlackList') const { SendB2BAppPushMessageService } = require('./SendB2BAppPushMessageService') const { SendB2CAppPushMessageService } = require('./SendB2CAppPushMessageService') +const { SendVoIPStartMessageService } = require('./SendVoIPStartMessageService') + /* AUTOGENERATE MARKER */ module.exports = { @@ -50,5 +52,6 @@ module.exports = { CustomValue, B2BAppPosIntegrationConfig, B2CAppAccessRightSet, + SendVoIPStartMessageService, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/config.js b/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/config.js index 04f48945f01..74600c99d7d 100644 --- a/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/config.js +++ b/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/config.js @@ -25,7 +25,7 @@ const B2C_APP_SERVICE_USER_ACCESS_AVAILABLE_SCHEMAS = { }, // Something that has B2CApp info and addressKey services: { - // sendVoIPStartPushMessage: {}, // NOTE(YEgorLu): to be made in DOMA-12905 + sendVoIPStartMessage: {}, // sendVoIPCancelPushMessage: {}, // NOTE(YEgorLu): to be made in DOMA-12906 }, } diff --git a/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/server.utils.js b/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/server.utils.js index f8947311363..b24d37394d4 100644 --- a/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/server.utils.js +++ b/apps/condo/domains/miniapp/utils/b2cAppServiceUserAccess/server.utils.js @@ -86,7 +86,7 @@ async function canReadByServiceUser (args, schemaConfig) { const permissionKey = `canRead${pluralize.plural(listKey)}` - return getFilterByFieldPathValue(pathToB2CApp, getB2CAppFilter({ user, requirePermission: !!schemaConfig.rightSetRequired, permissionKey })) + return getFilterByFieldPathValue(pathToB2CApp, getB2CAppFilter({ user, requirePermission: schemaConfig.rightSetRequired !== false, permissionKey })) } async function canManageByServiceUser ({ authentication: { item: user }, listKey, originalInput, itemId, itemIds, operation, context }, schemaConfig, parentSchemaName) { @@ -174,7 +174,7 @@ async function canManageByServiceUser ({ authentication: { item: user }, listKey const uniqueB2CAppIds = [...new Set(b2cAppIds)] const b2cApps = await find('B2CApp', { - ...getB2CAppFilter({ user, requirePermission: !!schemaConfig.rightSetRequired, permissionKey }), + ...getB2CAppFilter({ user, requirePermission: schemaConfig.rightSetRequired !== false, permissionKey }), id_in: uniqueB2CAppIds, deletedAt: null, }) @@ -207,7 +207,7 @@ async function canExecuteByServiceUser (params, serviceConfig) { deletedAt: null, addressKey, app: { - ...getB2CAppFilter({ user, requirePermission: !!serviceConfig.rightSetRequired, permissionKey }), + ...getB2CAppFilter({ user, requirePermission: serviceConfig.rightSetRequired !== false, permissionKey }), id: b2cAppId, deletedAt: null, }, diff --git a/apps/condo/domains/miniapp/utils/testSchema/index.js b/apps/condo/domains/miniapp/utils/testSchema/index.js index 35d495127bd..86354a41293 100644 --- a/apps/condo/domains/miniapp/utils/testSchema/index.js +++ b/apps/condo/domains/miniapp/utils/testSchema/index.js @@ -18,6 +18,7 @@ const { const { ALL_MINI_APPS_QUERY, SEND_B2C_APP_PUSH_MESSAGE_MUTATION, + SEND_VOIP_START_MESSAGE_MUTATION, B2BApp: B2BAppGQL, B2BAppContext: B2BAppContextGQL, B2BAppAccessRight: B2BAppAccessRightGQL, @@ -389,6 +390,22 @@ async function sendB2CAppPushMessageByTestClient (client, extraAttrs = {}) { return [data.result, attrs] } +async function sendVoIPStartMessageByTestClient (client, extraAttrs = {}) { + if (!client) throw new Error('no client') + + const sender = { dv: 1, fingerprint: faker.random.alphaNumeric(8) } + const attrs = { + dv: 1, + sender, + ...extraAttrs, + } + const { data, errors } = await client.mutate(SEND_VOIP_START_MESSAGE_MUTATION, { data: attrs }) + + throwIfError(data, errors, { query: SEND_VOIP_START_MESSAGE_MUTATION, variables: { data: attrs } }) + + return [data.result, attrs] +} + async function createTestMessageAppBlackList (client, extraAttrs = {}) { if (!client) throw new Error('no client') @@ -835,5 +852,6 @@ module.exports = { AppMessageSetting, createTestAppMessageSetting, updateTestAppMessageSetting, B2BAppPosIntegrationConfig, createTestB2BAppPosIntegrationConfig, updateTestB2BAppPosIntegrationConfig, B2CAppAccessRightSet, createTestB2CAppAccessRightSet, updateTestB2CAppAccessRightSet, + sendVoIPStartMessageByTestClient, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/miniapp/utils/voip.js b/apps/condo/domains/miniapp/utils/voip.js new file mode 100644 index 00000000000..503dda38e26 --- /dev/null +++ b/apps/condo/domains/miniapp/utils/voip.js @@ -0,0 +1,59 @@ +const { getKVClient } = require('@open-condo/keystone/kv') + +const KEY_PREFIX = 'voipCallStatus' +const kv = getKVClient(KEY_PREFIX) +const CALL_STATUS_TTL_IN_SECONDS = 60 * 3 + +const CALL_STATUS_START_SENT = 'CALL_STATUS_START_SENT' +const CALL_STATUS_CANCEL_SENT = 'CALL_STATUS_CANCEL_SENT' +const CALL_STATUSES = { + CALL_STATUS_START_SENT: CALL_STATUS_START_SENT, + CALL_STATUS_CANCEL_SENT: CALL_STATUS_CANCEL_SENT, +} + +function validateCallId (callId) { + return typeof callId === 'string' + && callId.length > 0 + && callId.length < 300 +} + +function normalizeCallId (callId) { + return callId + // eslint-disable-next-line no-control-regex + .replace(/[\u0000-\u001F\s]/g, '') +} + +function buildKey (b2cAppId, callId) { + return [KEY_PREFIX, b2cAppId, Buffer.from(callId).toString('base64')].join(':') +} + +// NOTE(YEgorLu): startingMessagesIdsByUserIds present for older mobile apps compatibility, as they use this to cancel calls. +// Will be removed someday in the future. Should be okay to save it only for few minutes +async function setCallStatus ({ b2cAppId, callId, status, startingMessagesIdsByUserIds }) { + if (!validateCallId(callId)) return false + return kv.set( + buildKey(b2cAppId, normalizeCallId(callId)), + JSON.stringify({ status, startingMessagesIdsByUserIds }), + 'EX', + CALL_STATUS_TTL_IN_SECONDS, + ) +} + +async function getCallStatus ({ b2cAppId, callId }) { + if (!validateCallId(callId)) return null + const res = await kv.get(buildKey(b2cAppId, normalizeCallId(callId))) + try { + return JSON.parse(res) + } catch { + return null + } +} + +module.exports = { + setCallStatus, + getCallStatus, + + CALL_STATUSES, + CALL_STATUS_START_SENT, + CALL_STATUS_CANCEL_SENT, +} \ No newline at end of file diff --git a/apps/condo/domains/notification/constants/constants.js b/apps/condo/domains/notification/constants/constants.js index cf06f14af07..c6913fa3b81 100644 --- a/apps/condo/domains/notification/constants/constants.js +++ b/apps/condo/domains/notification/constants/constants.js @@ -639,6 +639,8 @@ const MESSAGE_META = { voipLogin: { required: false }, voipPassword: { required: false }, voipDtfmCommand: { required: false }, + voipPanels: { required: false }, + stunServers: { required: false }, stun: { required: false }, codec: { required: false }, }, diff --git a/apps/condo/domains/property/utils/serverSchema/helpers.js b/apps/condo/domains/property/utils/serverSchema/helpers.js index 0a34d281a2f..9c75e614694 100644 --- a/apps/condo/domains/property/utils/serverSchema/helpers.js +++ b/apps/condo/domains/property/utils/serverSchema/helpers.js @@ -2,6 +2,10 @@ const { get, omitBy, isNull, isArray } = require('lodash') const FLAT_WITHOUT_FLAT_TYPE_MESSAGE = 'Flat is specified, but flat type is not!' +const { itemsQuery } = require('@open-condo/keystone/schema') + +const { MANAGING_COMPANY_TYPE } = require('@condo/domains/organization/constants/common') + /** * Sometimes address can contain flat with prefix, for example, in case of scanning receipt with QR-code. * Input data is out of control ;) @@ -102,10 +106,24 @@ const getUnitsFromSections = (sections = []) => { ).flat(2) } +async function getOldestNonDeletedProperty ({ addressKey }) { + const [oldestProperty] = await itemsQuery('Property', { + where: { + addressKey: addressKey, + deletedAt: null, + organization: { type: MANAGING_COMPANY_TYPE }, + }, + first: 1, + sortBy: ['isApproved_DESC', 'createdAt_ASC'], // sorting order is essential here + }) + return oldestProperty +} + module.exports = { FLAT_WITHOUT_FLAT_TYPE_MESSAGE, getAddressUpToBuildingFrom, normalizePropertyMap, getAddressDetails, getUnitsFromSections, + getOldestNonDeletedProperty, } diff --git a/apps/condo/lang/en/en.json b/apps/condo/lang/en/en.json index 71af4704005..e237d99e0a9 100644 --- a/apps/condo/lang/en/en.json +++ b/apps/condo/lang/en/en.json @@ -483,6 +483,10 @@ "api.miniapp.sendB2CAppPushMessage.RESIDENT_NOT_FOUND": "Unable to find resident by provided id.", "api.miniapp.sendB2CAppPushMessage.USER_NOT_FOUND": "Unable to find user by provided id.", "api.miniapp.sendB2CAppPushMessage.WRONG_SENDER_FORMAT": "Invalid format of \"sender\" field value. {details}", + "api.miniapp.sendVoIPStartMessage.PROPERTY_NOT_FOUND": "Building not found for the provided \"addressKey\"", + "api.miniapp.sendVoIPStartMessage.APP_NOT_FOUND": "Application not found for the provided \"id\"", + "api.miniapp.sendVoIPStartMessage.DV_VERSION_MISMATCH": "Invalid data version value", + "api.miniapp.sendVoIPStartMessage.WRONG_SENDER_FORMAT": "Invalid format for the \"sender\" field", "api.news.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE": "Response from NewsSharing miniapp was successful, but the data format was incorrect. Please consult with miniapp developer", "api.news.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_FAILED": "Could not get a successful response from NewsSharing miniapp. Please check the network and url configuration", "api.news.getNewsSharingRecipients.NOT_NEWS_SHARING_APP": "Provided b2bAppContext.app is not a NewsSharing miniapp", diff --git a/apps/condo/lang/es/es.json b/apps/condo/lang/es/es.json index 010711f283d..d952b8a2ef9 100644 --- a/apps/condo/lang/es/es.json +++ b/apps/condo/lang/es/es.json @@ -483,6 +483,10 @@ "api.miniapp.sendB2CAppPushMessage.RESIDENT_NOT_FOUND": "No se ha encontrado el vecino con el id introducido", "api.miniapp.sendB2CAppPushMessage.USER_NOT_FOUND": "No se ha encontrado el usuario con el id introducido", "api.miniapp.sendB2CAppPushMessage.WRONG_SENDER_FORMAT": "Formato del campo \"sender\" no válido", + "api.miniapp.sendVoIPStartMessage.PROPERTY_NOT_FOUND": "No se encontró el edificio para el \"addressKey\" proporcionado", + "api.miniapp.sendVoIPStartMessage.APP_NOT_FOUND": "No se encontró la aplicación para el \"id\" proporcionado", + "api.miniapp.sendVoIPStartMessage.DV_VERSION_MISMATCH": "Valor de versión de datos no válido", + "api.miniapp.sendVoIPStartMessage.WRONG_SENDER_FORMAT": "Formato no válido para el campo \"sender\"", "api.news.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE": "La solicitud a la miniapp ha devuelto datos incorrectos", "api.news.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_FAILED": "La solicitud a la miniapp se completó con un error.", "api.news.getNewsSharingRecipients.NOT_NEWS_SHARING_APP": "Esta miniapp no admite la funcionalidad de envío de noticias", diff --git a/apps/condo/lang/ru/ru.json b/apps/condo/lang/ru/ru.json index 6758c69beff..8d2775f6b58 100644 --- a/apps/condo/lang/ru/ru.json +++ b/apps/condo/lang/ru/ru.json @@ -483,6 +483,10 @@ "api.miniapp.sendB2CAppPushMessage.RESIDENT_NOT_FOUND": "Не удалось найти жителя по переданному id", "api.miniapp.sendB2CAppPushMessage.USER_NOT_FOUND": "Не удалось найти пользователя по переданному id", "api.miniapp.sendB2CAppPushMessage.WRONG_SENDER_FORMAT": "Неверный формат поля \"sender\"", + "api.miniapp.sendVoIPStartMessage.PROPERTY_NOT_FOUND": "Не удалось найти здание по переданному \"addressKey\"", + "api.miniapp.sendVoIPStartMessage.APP_NOT_FOUND": "Не удалось найти приложение по переданному \"id\"", + "api.miniapp.sendVoIPStartMessage.DV_VERSION_MISMATCH": "Неверное значение версии данных", + "api.miniapp.sendVoIPStartMessage.WRONG_SENDER_FORMAT": "Неверный формат поля \"sender\"", "api.news.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE": "Запрос в миниапп вернул некорректные данные", "api.news.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_FAILED": "Запрос в миниапп завершился с ошибкой", "api.news.getNewsSharingRecipients.NOT_NEWS_SHARING_APP": "Этот миниапп не поддерживает функциональность отправки новостей", diff --git a/apps/condo/migrations/20260420183110-0525_b2cappaccessrightset_canexecutesendvoipstartmessage_and_more.js b/apps/condo/migrations/20260420183110-0525_b2cappaccessrightset_canexecutesendvoipstartmessage_and_more.js new file mode 100644 index 00000000000..35aff361953 --- /dev/null +++ b/apps/condo/migrations/20260420183110-0525_b2cappaccessrightset_canexecutesendvoipstartmessage_and_more.js @@ -0,0 +1,48 @@ +// auto generated by kmigrator +// KMIGRATOR:0525_b2cappaccessrightset_canexecutesendvoipstartmessage_and_more:IyBHZW5lcmF0ZWQgYnkgRGphbmdvIDUuMS41IG9uIDIwMjYtMDQtMjAgMTM6MzEKCmZyb20gZGphbmdvLmRiIGltcG9ydCBtaWdyYXRpb25zLCBtb2RlbHMKCgpjbGFzcyBNaWdyYXRpb24obWlncmF0aW9ucy5NaWdyYXRpb24pOgoKICAgIGRlcGVuZGVuY2llcyA9IFsKICAgICAgICAoJ19kamFuZ29fc2NoZW1hJywgJzA1MjRfYjJjYXBwYWNjZXNzcmlnaHRzZXRoaXN0b3J5cmVjb3JkX2FuZF9tb3JlJyksCiAgICBdCgogICAgb3BlcmF0aW9ucyA9IFsKICAgICAgICBtaWdyYXRpb25zLkFkZEZpZWxkKAogICAgICAgICAgICBtb2RlbF9uYW1lPSdiMmNhcHBhY2Nlc3NyaWdodHNldCcsCiAgICAgICAgICAgIG5hbWU9J2NhbkV4ZWN1dGVTZW5kVm9JUFN0YXJ0TWVzc2FnZScsCiAgICAgICAgICAgIGZpZWxkPW1vZGVscy5Cb29sZWFuRmllbGQoZGVmYXVsdD1GYWxzZSksCiAgICAgICAgKSwKICAgICAgICBtaWdyYXRpb25zLkFkZEZpZWxkKAogICAgICAgICAgICBtb2RlbF9uYW1lPSdiMmNhcHBhY2Nlc3NyaWdodHNldGhpc3RvcnlyZWNvcmQnLAogICAgICAgICAgICBuYW1lPSdjYW5FeGVjdXRlU2VuZFZvSVBTdGFydE1lc3NhZ2UnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSksCiAgICAgICAgKSwKICAgIF0K +// KMIGRATOR_VIEWS:0525_b2cappaccessrightset_canexecutesendvoipstartmessage_and_more:eyJkdiI6MSwibGlzdHMiOnsiVXNlciI6eyJmaWVsZHMiOlsiYXZhdGFyIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiY3VzdG9tQWNjZXNzIiwiZGVsZXRlZEF0IiwiZHYiLCJlbWFpbCIsImV4dGVybmFsRW1haWwiLCJleHRlcm5hbFBob25lIiwiZXh0ZXJuYWxTeXN0ZW1OYW1lIiwiaGFzTWFya2V0aW5nQ29uc2VudCIsImlkIiwiaXNBZG1pbiIsImlzRW1haWxWZXJpZmllZCIsImlzRXh0ZXJuYWxFbWFpbFZlcmlmaWVkIiwiaXNFeHRlcm5hbFBob25lVmVyaWZpZWQiLCJpc1Bob25lVmVyaWZpZWQiLCJpc1N1cHBvcnQiLCJpc1R3b0ZhY3RvckF1dGhlbnRpY2F0aW9uRW5hYmxlZCIsImxvY2FsZSIsIm1ldGEiLCJuYW1lIiwibmV3SWQiLCJwYXNzd29yZCIsInBob25lIiwicmlnaHRzU2V0Iiwic2VuZGVyIiwic2hvd0dsb2JhbEhpbnRzIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImVtYWlsIiwiZXh0ZXJuYWxFbWFpbCIsImV4dGVybmFsUGhvbmUiLCJtZXRhIiwibmFtZSIsInBhc3N3b3JkIiwicGhvbmUiXX0sIlVzZXJFeHRlcm5hbElkZW50aXR5Ijp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaWRlbnRpdHlJZCIsImlkZW50aXR5VHlwZSIsIm1ldGEiLCJuZXdJZCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ1c2VyVHlwZSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImlkZW50aXR5SWQiLCJtZXRhIl19LCJVc2VyUmlnaHRzU2V0Ijp7ImZpZWxkcyI6WyJjYW5FeGVjdXRlR2V0QXZhaWxhYmxlU3Vic2NyaXB0aW9uUGxhbnMiLCJjYW5FeGVjdXRlUmVnaXN0ZXJOZXdTZXJ2aWNlVXNlciIsImNhbkV4ZWN1dGVSZWdpc3RlclN1YnNjcmlwdGlvbkNvbnRleHQiLCJjYW5FeGVjdXRlU2VuZE1lc3NhZ2UiLCJjYW5FeGVjdXRlX2FsbEJpbGxpbmdSZWNlaXB0c1N1bSIsImNhbkV4ZWN1dGVfYWxsUGF5bWVudHNTdW0iLCJjYW5FeGVjdXRlX2ludGVybmFsU2VuZEhhc2hlZFJlc2lkZW50UGhvbmVzIiwiY2FuTWFuYWdlQjJCQXBwQWNjZXNzUmlnaHRTZXRzIiwiY2FuTWFuYWdlQjJCQXBwQWNjZXNzUmlnaHRzIiwiY2FuTWFuYWdlQjJCQXBwQ29udGV4dHMiLCJjYW5NYW5hZ2VCMkJBcHBOZXdzU2hhcmluZ0NvbmZpZ3MiLCJjYW5NYW5hZ2VCMkJBcHBQZXJtaXNzaW9ucyIsImNhbk1hbmFnZUIyQkFwcFByb21vQmxvY2tzIiwiY2FuTWFuYWdlQjJCQXBwcyIsImNhbk1hbmFnZUIyQ0FwcEFjY2Vzc1JpZ2h0cyIsImNhbk1hbmFnZUIyQ0FwcEJ1aWxkcyIsImNhbk1hbmFnZUIyQ0FwcFByb3BlcnRpZXMiLCJjYW5NYW5hZ2VCMkNBcHBzIiwiY2FuTWFuYWdlQmlsbGluZ0ludGVncmF0aW9uT3JnYW5pemF0aW9uQ29udGV4dERlbGV0ZWRBdEZpZWxkIiwiY2FuTWFuYWdlTWVzc2FnZUJhdGNoZXMiLCJjYW5NYW5hZ2VPaWRjQ2xpZW50cyIsImNhbk1hbmFnZU9yZ2FuaXphdGlvbklzQXBwcm92ZWRGaWVsZCIsImNhbk1hbmFnZU9yZ2FuaXphdGlvbnMiLCJjYW5NYW5hZ2VQcm9wZXJ0aWVzIiwiY2FuTWFuYWdlUmVzZXRVc2VyTGltaXRBY3Rpb25zIiwiY2FuTWFuYWdlVGlja2V0QXV0b0Fzc2lnbm1lbnRzIiwiY2FuTWFuYWdlVGlja2V0U2VudFRvQXV0aG9yaXRpZXNBdEZpZWxkIiwiY2FuTWFuYWdlVGlja2V0cyIsImNhbk1hbmFnZVVzZXJIYXNNYXJrZXRpbmdDb25zZW50RmllbGQiLCJjYW5NYW5hZ2VVc2VyUmlnaHRzU2V0RmllbGQiLCJjYW5NYW5hZ2VVc2VyUmlnaHRzU2V0cyIsImNhblJlYWRCMkJBcHBBY2Nlc3NSaWdodFNldHMiLCJjYW5SZWFkQjJCQXBwQWNjZXNzUmlnaHRzIiwiY2FuUmVhZEIyQkFwcENvbnRleHRzIiwiY2FuUmVhZEIyQkFwcE5ld3NTaGFyaW5nQ29uZmlncyIsImNhblJlYWRCMkJBcHBQZXJtaXNzaW9ucyIsImNhblJlYWRCMkJBcHBQcm9tb0Jsb2NrcyIsImNhblJlYWRCMkJBcHBzIiwiY2FuUmVhZEIyQ0FwcEFjY2Vzc1JpZ2h0cyIsImNhblJlYWRCMkNBcHBCdWlsZHMiLCJjYW5SZWFkQjJDQXBwUHJvcGVydGllcyIsImNhblJlYWRCMkNBcHBzIiwiY2FuUmVhZEJpbGxpbmdJbnRlZ3JhdGlvbk9yZ2FuaXphdGlvbkNvbnRleHRzIiwiY2FuUmVhZEJpbGxpbmdSZWNlaXB0cyIsImNhblJlYWRNZXNzYWdlQmF0Y2hlcyIsImNhblJlYWRNZXNzYWdlcyIsImNhblJlYWRPaWRjQ2xpZW50cyIsImNhblJlYWRPcmdhbml6YXRpb25zIiwiY2FuUmVhZFBheW1lbnRzIiwiY2FuUmVhZFByb3BlcnRpZXMiLCJjYW5SZWFkUmVzZXRVc2VyTGltaXRBY3Rpb25zIiwiY2FuUmVhZFJlc2lkZW50cyIsImNhblJlYWRUaWNrZXRBdXRvQXNzaWdubWVudHMiLCJjYW5SZWFkVGlja2V0cyIsImNhblJlYWRVc2VyRW1haWxGaWVsZCIsImNhblJlYWRVc2VyUGhvbmVGaWVsZCIsImNhblJlYWRVc2VyUmlnaHRzU2V0cyIsImNhblJlYWRVc2VycyIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJjYW5FeGVjdXRlX2ludGVybmFsU2VuZEhhc2hlZFJlc2lkZW50UGhvbmVzIiwiY2FuUmVhZFVzZXJFbWFpbEZpZWxkIiwiY2FuUmVhZFVzZXJQaG9uZUZpZWxkIl19LCJPcmdhbml6YXRpb24iOnsiZmllbGRzIjpbImF2YXRhciIsImNvdW50cnkiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkZXNjcmlwdGlvbiIsImR2IiwiZmVhdHVyZXMiLCJpZCIsImltcG9ydElkIiwiaW1wb3J0UmVtb3RlU3lzdGVtIiwiaXNBcHByb3ZlZCIsIm1ldGEiLCJuYW1lIiwibmV3SWQiLCJwaG9uZSIsInBob25lTnVtYmVyUHJlZml4Iiwic2VuZGVyIiwidGluIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbInBob25lIiwicGhvbmVOdW1iZXJQcmVmaXgiXX0sIk9yZ2FuaXphdGlvbkVtcGxveWVlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImVtYWlsIiwiaGFzQWxsU3BlY2lhbGl6YXRpb25zIiwiaWQiLCJpbnZpdGVDb2RlIiwiaXNBY2NlcHRlZCIsImlzQmxvY2tlZCIsImlzUmVqZWN0ZWQiLCJuYW1lIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJwaG9uZSIsInBvc2l0aW9uIiwicm9sZSIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJlbWFpbCIsImludml0ZUNvZGUiLCJuYW1lIiwicGhvbmUiXX0sIk9yZ2FuaXphdGlvbkVtcGxveWVlUm9sZSI6eyJmaWVsZHMiOlsiY2FuQmVBc3NpZ25lZEFzRXhlY3V0b3IiLCJjYW5CZUFzc2lnbmVkQXNSZXNwb25zaWJsZSIsImNhbkRvd25sb2FkQ2FsbFJlY29yZHMiLCJjYW5JbXBvcnRCaWxsaW5nUmVjZWlwdHMiLCJjYW5JbnZpdGVOZXdPcmdhbml6YXRpb25FbXBsb3llZXMiLCJjYW5NYW5hZ2VCMkJBcHBzIiwiY2FuTWFuYWdlQmFua0FjY291bnRSZXBvcnRUYXNrcyIsImNhbk1hbmFnZUJhbmtBY2NvdW50UmVwb3J0cyIsImNhbk1hbmFnZUJhbmtBY2NvdW50cyIsImNhbk1hbmFnZUJhbmtDb250cmFjdG9yQWNjb3VudHMiLCJjYW5NYW5hZ2VCYW5rSW50ZWdyYXRpb25BY2NvdW50Q29udGV4dHMiLCJjYW5NYW5hZ2VCYW5rSW50ZWdyYXRpb25Pcmdhbml6YXRpb25Db250ZXh0cyIsImNhbk1hbmFnZUJhbmtUcmFuc2FjdGlvbnMiLCJjYW5NYW5hZ2VDYWxsUmVjb3JkcyIsImNhbk1hbmFnZUNvbnRhY3RSb2xlcyIsImNhbk1hbmFnZUNvbnRhY3RzIiwiY2FuTWFuYWdlRG9jdW1lbnRzIiwiY2FuTWFuYWdlRW1wbG95ZWVzIiwiY2FuTWFuYWdlSW5jaWRlbnRzIiwiY2FuTWFuYWdlSW50ZWdyYXRpb25zIiwiY2FuTWFuYWdlSW52b2ljZXMiLCJjYW5NYW5hZ2VNYXJrZXRJdGVtUHJpY2VzIiwiY2FuTWFuYWdlTWFya2V0SXRlbXMiLCJjYW5NYW5hZ2VNYXJrZXRQcmljZVNjb3BlcyIsImNhbk1hbmFnZU1hcmtldFNldHRpbmciLCJjYW5NYW5hZ2VNYXJrZXRwbGFjZSIsImNhbk1hbmFnZU1ldGVyUmVhZGluZ3MiLCJjYW5NYW5hZ2VNZXRlcnMiLCJjYW5NYW5hZ2VNb2JpbGVGZWF0dXJlQ29uZmlncyIsImNhbk1hbmFnZU5ld3NJdGVtVGVtcGxhdGVzIiwiY2FuTWFuYWdlTmV3c0l0ZW1zIiwiY2FuTWFuYWdlT3JnYW5pemF0aW9uIiwiY2FuTWFuYWdlT3JnYW5pemF0aW9uRW1wbG95ZWVSZXF1ZXN0cyIsImNhbk1hbmFnZVByb3BlcnRpZXMiLCJjYW5NYW5hZ2VQcm9wZXJ0eVNjb3BlcyIsImNhbk1hbmFnZVJvbGVzIiwiY2FuTWFuYWdlU3Vic2NyaXB0aW9ucyIsImNhbk1hbmFnZVRpY2tldEF1dG9Bc3NpZ25tZW50cyIsImNhbk1hbmFnZVRpY2tldENvbW1lbnRzIiwiY2FuTWFuYWdlVGlja2V0UHJvcGVydHlIaW50cyIsImNhbk1hbmFnZVRpY2tldHMiLCJjYW5NYW5hZ2VUb3VyIiwiY2FuUmVhZEFuYWx5dGljcyIsImNhblJlYWRCaWxsaW5nUmVjZWlwdHMiLCJjYW5SZWFkQ2FsbFJlY29yZHMiLCJjYW5SZWFkQ29udGFjdHMiLCJjYW5SZWFkRG9jdW1lbnRzIiwiY2FuUmVhZEVtcGxveWVlcyIsImNhblJlYWRFeHRlcm5hbFJlcG9ydHMiLCJjYW5SZWFkSW5jaWRlbnRzIiwiY2FuUmVhZEludm9pY2VzIiwiY2FuUmVhZE1hcmtldEl0ZW1QcmljZXMiLCJjYW5SZWFkTWFya2V0SXRlbXMiLCJjYW5SZWFkTWFya2V0UHJpY2VTY29wZXMiLCJjYW5SZWFkTWFya2V0U2V0dGluZyIsImNhblJlYWRNYXJrZXRwbGFjZSIsImNhblJlYWRNZXRlcnMiLCJjYW5SZWFkTmV3c0l0ZW1zIiwiY2FuUmVhZFBheW1lbnRzIiwiY2FuUmVhZFBheW1lbnRzV2l0aEludm9pY2VzIiwiY2FuUmVhZFByb3BlcnRpZXMiLCJjYW5SZWFkU2VydmljZXMiLCJjYW5SZWFkU2V0dGluZ3MiLCJjYW5SZWFkVGlja2V0cyIsImNhblJlYWRUb3VyIiwiY2FuU2hhcmVUaWNrZXRzIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZGVzY3JpcHRpb24iLCJkdiIsImlkIiwiaXNEZWZhdWx0IiwiaXNFZGl0YWJsZSIsIm5hbWUiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInRpY2tldFZpc2liaWxpdHlUeXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiY2FuUmVhZFNldHRpbmdzIl19LCJPcmdhbml6YXRpb25MaW5rIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImZyb20iLCJpZCIsIm5ld0lkIiwic2VuZGVyIiwidG8iLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJPcmdhbml6YXRpb25FbXBsb3llZVNwZWNpYWxpemF0aW9uIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImVtcGxveWVlIiwiaWQiLCJuZXdJZCIsInNlbmRlciIsInNwZWNpYWxpemF0aW9uIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiT3JnYW5pemF0aW9uRW1wbG95ZWVSZXF1ZXN0Ijp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJjcmVhdGVkRW1wbG95ZWUiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaXNBY2NlcHRlZCIsImlzUmVqZWN0ZWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsIm9yZ2FuaXphdGlvbklkIiwib3JnYW5pemF0aW9uTmFtZSIsIm9yZ2FuaXphdGlvblRpbiIsInByb2Nlc3NlZEF0IiwicHJvY2Vzc2VkQnkiLCJyZXRyaWVzIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInVzZXJOYW1lIiwidXNlclBob25lIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsidXNlck5hbWUiLCJ1c2VyUGhvbmUiXX0sIlByb3BlcnR5Ijp7ImZpZWxkcyI6WyJhZGRyZXNzIiwiYWRkcmVzc0tleSIsImFkZHJlc3NNZXRhIiwiYWRkcmVzc1NvdXJjZXMiLCJhcmVhIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImlzQXBwcm92ZWQiLCJtYXAiLCJuYW1lIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ0eXBlIiwidW5pbmhhYml0ZWRVbml0c0NvdW50IiwidW5pdHNDb3VudCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiLCJ5ZWFyT2ZDb25zdHJ1Y3Rpb24iXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkJpbGxpbmdJbnRlZ3JhdGlvbiI6eyJmaWVsZHMiOlsiYXBwVXJsIiwiYjJiQXBwIiwiYmFubmVyQ29sb3IiLCJiYW5uZXJQcm9tb0ltYWdlIiwiYmFubmVyVGV4dENvbG9yIiwiYmlsbGluZ1BhZ2VJY29uIiwiYmlsbGluZ1BhZ2VUaXRsZSIsImNoZWNrQWNjb3VudE51bWJlclVybCIsImNoZWNrQWRkcmVzc1VybCIsImNvbm5lY3RlZE1lc3NhZ2UiLCJjb25uZWN0ZWRVcmwiLCJjb250ZXh0RGVmYXVsdFN0YXR1cyIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1cnJlbmN5Q29kZSIsImRhdGFGb3JtYXQiLCJkZWxldGVkQXQiLCJkZXRhaWxlZERlc2NyaXB0aW9uIiwiZHYiLCJleHRlbmRzQmlsbGluZ1BhZ2UiLCJncm91cCIsImlkIiwiaW5zdHJ1Y3Rpb24iLCJpbnN0cnVjdGlvbkV4dHJhTGluayIsImlzSGlkZGVuIiwiaXNUcnVzdGVkQmFua0FjY291bnRTb3VyY2UiLCJsb2dvIiwibmFtZSIsIm5ld0lkIiwicmVjZWlwdHNMb2FkaW5nVGltZSIsInNlbmRlciIsInNldHVwVXJsIiwic2hvcnREZXNjcmlwdGlvbiIsInNraXBOb0FjY291bnROb3RpZmljYXRpb25zIiwidGFyZ2V0RGVzY3JpcHRpb24iLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1cGxvYWRNZXNzYWdlIiwidXBsb2FkVXJsIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQmlsbGluZ0ludGVncmF0aW9uQWNjZXNzUmlnaHQiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJpbnRlZ3JhdGlvbiIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkJpbGxpbmdJbnRlZ3JhdGlvbk9yZ2FuaXphdGlvbkNvbnRleHQiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1cnJlbnRQcm9ibGVtIiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImludGVncmF0aW9uIiwibGFzdFJlcG9ydCIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwic2V0dGluZ3MiLCJzdGF0ZSIsInN0YXR1cyIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbInNldHRpbmdzIiwic3RhdGUiXX0sIkJpbGxpbmdJbnRlZ3JhdGlvblByb2JsZW0iOnsiZmllbGRzIjpbImNvbnRleHQiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibWVzc2FnZSIsIm1ldGEiLCJuZXdJZCIsInNlbmRlciIsInRpdGxlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQmlsbGluZ1Byb3BlcnR5Ijp7ImZpZWxkcyI6WyJhZGRyZXNzIiwiYWRkcmVzc0tleSIsImFkZHJlc3NNZXRhIiwiYWRkcmVzc1NvdXJjZXMiLCJjb250ZXh0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJnbG9iYWxJZCIsImlkIiwiaW1wb3J0SWQiLCJtZXRhIiwibmV3SWQiLCJub3JtYWxpemVkQWRkcmVzcyIsInJhdyIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbIm1ldGEiLCJyYXciXX0sIkJpbGxpbmdBY2NvdW50Ijp7ImZpZWxkcyI6WyJjb250ZXh0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJmdWxsTmFtZSIsImdsb2JhbElkIiwiaWQiLCJpbXBvcnRJZCIsImlzQ2xvc2VkIiwibWV0YSIsIm5ld0lkIiwibnVtYmVyIiwib3duZXJUeXBlIiwicHJvcGVydHkiLCJyYXciLCJzZW5kZXIiLCJ1bml0TmFtZSIsInVuaXRUeXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZnVsbE5hbWUiLCJtZXRhIiwicmF3Il19LCJCaWxsaW5nUmVjZWlwdCI6eyJmaWVsZHMiOlsiYWNjb3VudCIsImFtb3VudERpc3RyaWJ1dGlvbiIsImJhbGFuY2UiLCJiYWxhbmNlVXBkYXRlZEF0IiwiY2F0ZWdvcnkiLCJjaGFyZ2UiLCJjb250ZXh0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJmaWxlIiwiZm9ybXVsYSIsImlkIiwiaW1wb3J0SWQiLCJuZXdJZCIsInBhaWQiLCJwYXltZW50U3RhdHVzQ2hhbmdlV2ViaG9va1NlY3JldCIsInBheW1lbnRTdGF0dXNDaGFuZ2VXZWJob29rVXJsIiwicGVuYWx0eSIsInBlcmlvZCIsInByaW50YWJsZU51bWJlciIsInByaXZpbGVnZSIsInByb3BlcnR5IiwicmF3IiwicmVjYWxjdWxhdGlvbiIsInJlY2VpdmVyIiwicmVjaXBpZW50Iiwic2VuZGVyIiwic2VydmljZXMiLCJ0b1BheSIsInRvUGF5RGV0YWlscyIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImFtb3VudERpc3RyaWJ1dGlvbiIsInBheW1lbnRTdGF0dXNDaGFuZ2VXZWJob29rU2VjcmV0IiwicGF5bWVudFN0YXR1c0NoYW5nZVdlYmhvb2tVcmwiLCJyYXciLCJzZXJ2aWNlcyIsInRvUGF5RGV0YWlscyJdfSwiQmlsbGluZ1JlY2lwaWVudCI6eyJmaWVsZHMiOlsiYmFua0FjY291bnQiLCJiYW5rTmFtZSIsImJpYyIsImNsYXNzaWZpY2F0aW9uQ29kZSIsImNvbnRleHQiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaWVjIiwiaW1wb3J0SWQiLCJtZXRhIiwibmFtZSIsIm5ld0lkIiwib2Zmc2V0dGluZ0FjY291bnQiLCJwdXJwb3NlIiwic2VuZGVyIiwidGVycml0b3J5Q29kZSIsInRpbiIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbIm1ldGEiXX0sIkJpbGxpbmdDYXRlZ29yeSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5hbWUiLCJuZXdJZCIsInJlY2VpcHRWYWxpZGl0eU1vbnRocyIsInJlcXVpcmVzRnVsbFBheW1lbnQiLCJzZW5kZXIiLCJzZXJ2aWNlTmFtZXMiLCJza2lwTm90aWZpY2F0aW9ucyIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkJhbmtBY2NvdW50Ijp7ImZpZWxkcyI6WyJhcHByb3ZlZEF0IiwiYXBwcm92ZWRCeSIsImJhbmtOYW1lIiwiY2xhc3NpZmljYXRpb25Db2RlIiwiY291bnRyeSIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1cnJlbmN5Q29kZSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJpbXBvcnRJZCIsImludGVncmF0aW9uQ29udGV4dCIsImlzQXBwcm92ZWQiLCJtZXRhIiwibmFtZSIsIm5ld0lkIiwibnVtYmVyIiwib3JnYW5pemF0aW9uIiwicHJvcGVydHkiLCJyb3V0aW5nTnVtYmVyIiwicm91dGluZ051bWJlck1ldGEiLCJzZW5kZXIiLCJ0ZXJyaXRvcnlDb2RlIiwidGluIiwidGluTWV0YSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbIm1ldGEiLCJyb3V0aW5nTnVtYmVyTWV0YSIsInRpbk1ldGEiXX0sIkJhbmtDYXRlZ29yeSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5hbWUiLCJuZXdJZCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkJhbmtDb3N0SXRlbSI6eyJmaWVsZHMiOlsiY2F0ZWdvcnkiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaXNPdXRjb21lIiwibmFtZSIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQmFua0NvbnRyYWN0b3JBY2NvdW50Ijp7ImZpZWxkcyI6WyJiYW5rTmFtZSIsImNvc3RJdGVtIiwiY291bnRyeSIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1cnJlbmN5Q29kZSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJpbXBvcnRJZCIsIm1ldGEiLCJuYW1lIiwibmV3SWQiLCJudW1iZXIiLCJvcmdhbml6YXRpb24iLCJyb3V0aW5nTnVtYmVyIiwic2VuZGVyIiwidGVycml0b3J5Q29kZSIsInRpbiIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbIm1ldGEiXX0sIkJhbmtJbnRlZ3JhdGlvbiI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5hbWUiLCJuZXdJZCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkJhbmtJbnRlZ3JhdGlvbkFjY2Vzc1JpZ2h0Ijp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaW50ZWdyYXRpb24iLCJuZXdJZCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJCYW5rSW50ZWdyYXRpb25BY2NvdW50Q29udGV4dCI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJlbmFibGVkIiwiaWQiLCJpbnRlZ3JhdGlvbiIsIm1ldGEiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbIm1ldGEiXX0sIkJhbmtUcmFuc2FjdGlvbiI6eyJmaWVsZHMiOlsiYWNjb3VudCIsImFtb3VudCIsImNvbnRyYWN0b3JBY2NvdW50IiwiY29zdEl0ZW0iLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJjdXJyZW5jeUNvZGUiLCJkYXRlIiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImltcG9ydElkIiwiaW1wb3J0UmVtb3RlU3lzdGVtIiwiaW50ZWdyYXRpb25Db250ZXh0IiwiaXNPdXRjb21lIiwibWV0YSIsIm5ld0lkIiwibnVtYmVyIiwib3JnYW5pemF0aW9uIiwicHVycG9zZSIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbIm1ldGEiXX0sIkJhbmtJbnRlZ3JhdGlvbk9yZ2FuaXphdGlvbkNvbnRleHQiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZW5hYmxlZCIsImlkIiwiaW50ZWdyYXRpb24iLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlRpY2tldCI6eyJmaWVsZHMiOlsiYXNzaWduZWUiLCJjYW5SZWFkQnlSZXNpZGVudCIsImNhdGVnb3J5Q2xhc3NpZmllciIsImNsYXNzaWZpZXIiLCJjbGllbnQiLCJjbGllbnRFbWFpbCIsImNsaWVudE5hbWUiLCJjbGllbnRQaG9uZSIsImNvbXBsZXRlZEF0IiwiY29udGFjdCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1c3RvbUNsYXNzaWZpZXIiLCJkZWFkbGluZSIsImRlZmVycmVkVW50aWwiLCJkZWxldGVkQXQiLCJkZXRhaWxzIiwiZHYiLCJleGVjdXRvciIsImZlZWRiYWNrQWRkaXRpb25hbE9wdGlvbnMiLCJmZWVkYmFja0NvbW1lbnQiLCJmZWVkYmFja1VwZGF0ZWRBdCIsImZlZWRiYWNrVmFsdWUiLCJmbG9vck5hbWUiLCJpZCIsImlzQXV0b0NsYXNzaWZpZWQiLCJpc0NvbXBsZXRlZEFmdGVyRGVhZGxpbmUiLCJpc0VtZXJnZW5jeSIsImlzSW5zdXJhbmNlIiwiaXNQYWlkIiwiaXNQYXlhYmxlIiwiaXNSZXNpZGVudFRpY2tldCIsImlzV2FycmFudHkiLCJrYW5iYW5Db2x1bW4iLCJrYW5iYW5PcmRlciIsImxhc3RDb21tZW50QXQiLCJsYXN0Q29tbWVudFdpdGhPcmdhbml6YXRpb25UeXBlQXQiLCJsYXN0Q29tbWVudFdpdGhSZXNpZGVudFR5cGVBdCIsImxhc3RDb21tZW50V2l0aFJlc2lkZW50VHlwZUNyZWF0ZWRCeVVzZXJUeXBlIiwibGFzdFJlc2lkZW50Q29tbWVudEF0IiwibWV0YSIsIm5ld0lkIiwibnVtYmVyIiwib3JkZXIiLCJvcmdhbml6YXRpb24iLCJwbGFjZUNsYXNzaWZpZXIiLCJwcmlvcml0eSIsInByb2JsZW1DbGFzc2lmaWVyIiwicHJvcGVydHkiLCJwcm9wZXJ0eUFkZHJlc3MiLCJwcm9wZXJ0eUFkZHJlc3NNZXRhIiwicXVhbGl0eUNvbnRyb2xBZGRpdGlvbmFsT3B0aW9ucyIsInF1YWxpdHlDb250cm9sQ29tbWVudCIsInF1YWxpdHlDb250cm9sVXBkYXRlZEF0IiwicXVhbGl0eUNvbnRyb2xVcGRhdGVkQnkiLCJxdWFsaXR5Q29udHJvbFZhbHVlIiwicmVsYXRlZCIsInJldmlld0NvbW1lbnQiLCJyZXZpZXdWYWx1ZSIsInNlY3Rpb25OYW1lIiwic2VjdGlvblR5cGUiLCJzZW5kZXIiLCJzZW50VG9BdXRob3JpdGllc0F0Iiwic291cmNlIiwic291cmNlTWV0YSIsInN0YXR1cyIsInN0YXR1c1JlYXNvbiIsInN0YXR1c1Jlb3BlbmVkQ291bnRlciIsInN0YXR1c1VwZGF0ZWRBdCIsInRpdGxlIiwidW5pdE5hbWUiLCJ1bml0VHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImNsaWVudEVtYWlsIiwiY2xpZW50TmFtZSIsImNsaWVudFBob25lIiwiZGV0YWlscyIsIm1ldGEiLCJzb3VyY2VNZXRhIiwidGl0bGUiXX0sIlRpY2tldFNvdXJjZSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImlzRGVmYXVsdCIsIm5hbWUiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJUaWNrZXRTdGF0dXMiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ0eXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiVGlja2V0RmlsZSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJmaWxlIiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInRpY2tldCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImZpbGUiXX0sIlRpY2tldENoYW5nZSI6eyJmaWVsZHMiOlsiYWN0dWFsQ3JlYXRpb25EYXRlIiwiYXNzaWduZWVEaXNwbGF5TmFtZUZyb20iLCJhc3NpZ25lZURpc3BsYXlOYW1lVG8iLCJhc3NpZ25lZUlkRnJvbSIsImFzc2lnbmVlSWRUbyIsImNhblJlYWRCeVJlc2lkZW50RnJvbSIsImNhblJlYWRCeVJlc2lkZW50VG8iLCJjbGFzc2lmaWVyRGlzcGxheU5hbWVGcm9tIiwiY2xhc3NpZmllckRpc3BsYXlOYW1lVG8iLCJjbGFzc2lmaWVySWRGcm9tIiwiY2xhc3NpZmllcklkVG8iLCJjbGllbnREaXNwbGF5TmFtZUZyb20iLCJjbGllbnREaXNwbGF5TmFtZVRvIiwiY2xpZW50RW1haWxGcm9tIiwiY2xpZW50RW1haWxUbyIsImNsaWVudElkRnJvbSIsImNsaWVudElkVG8iLCJjbGllbnROYW1lRnJvbSIsImNsaWVudE5hbWVUbyIsImNsaWVudFBob25lRnJvbSIsImNsaWVudFBob25lVG8iLCJjb250YWN0RGlzcGxheU5hbWVGcm9tIiwiY29udGFjdERpc3BsYXlOYW1lVG8iLCJjb250YWN0SWRGcm9tIiwiY29udGFjdElkVG8iLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJjdXN0b21DbGFzc2lmaWVyRnJvbSIsImN1c3RvbUNsYXNzaWZpZXJUbyIsImRlYWRsaW5lRnJvbSIsImRlYWRsaW5lVG8iLCJkZWZlcnJlZFVudGlsRnJvbSIsImRlZmVycmVkVW50aWxUbyIsImRldGFpbHNGcm9tIiwiZGV0YWlsc1RvIiwiZHYiLCJleGVjdXRvckRpc3BsYXlOYW1lRnJvbSIsImV4ZWN1dG9yRGlzcGxheU5hbWVUbyIsImV4ZWN1dG9ySWRGcm9tIiwiZXhlY3V0b3JJZFRvIiwiZmVlZGJhY2tBZGRpdGlvbmFsT3B0aW9uc0Zyb20iLCJmZWVkYmFja0FkZGl0aW9uYWxPcHRpb25zVG8iLCJmZWVkYmFja0NvbW1lbnRGcm9tIiwiZmVlZGJhY2tDb21tZW50VG8iLCJmZWVkYmFja1ZhbHVlRnJvbSIsImZlZWRiYWNrVmFsdWVUbyIsImZsb29yTmFtZUZyb20iLCJmbG9vck5hbWVUbyIsImlkIiwiaXNFbWVyZ2VuY3lGcm9tIiwiaXNFbWVyZ2VuY3lUbyIsImlzSW5zdXJhbmNlRnJvbSIsImlzSW5zdXJhbmNlVG8iLCJpc1BhaWRGcm9tIiwiaXNQYWlkVG8iLCJpc1BheWFibGVGcm9tIiwiaXNQYXlhYmxlVG8iLCJpc1Jlc2lkZW50VGlja2V0RnJvbSIsImlzUmVzaWRlbnRUaWNrZXRUbyIsImlzV2FycmFudHlGcm9tIiwiaXNXYXJyYW50eVRvIiwia2FuYmFuQ29sdW1uRnJvbSIsImthbmJhbkNvbHVtblRvIiwia2FuYmFuT3JkZXJGcm9tIiwia2FuYmFuT3JkZXJUbyIsIm1ldGFGcm9tIiwibWV0YVRvIiwib2JzZXJ2ZXJzRGlzcGxheU5hbWVzRnJvbSIsIm9ic2VydmVyc0Rpc3BsYXlOYW1lc1RvIiwib2JzZXJ2ZXJzSWRzRnJvbSIsIm9ic2VydmVyc0lkc1RvIiwib3JnYW5pemF0aW9uRGlzcGxheU5hbWVGcm9tIiwib3JnYW5pemF0aW9uRGlzcGxheU5hbWVUbyIsIm9yZ2FuaXphdGlvbklkRnJvbSIsIm9yZ2FuaXphdGlvbklkVG8iLCJwcmlvcml0eUZyb20iLCJwcmlvcml0eVRvIiwicHJvcGVydHlBZGRyZXNzRnJvbSIsInByb3BlcnR5QWRkcmVzc01ldGFGcm9tIiwicHJvcGVydHlBZGRyZXNzTWV0YVRvIiwicHJvcGVydHlBZGRyZXNzVG8iLCJwcm9wZXJ0eURpc3BsYXlOYW1lRnJvbSIsInByb3BlcnR5RGlzcGxheU5hbWVUbyIsInByb3BlcnR5SWRGcm9tIiwicHJvcGVydHlJZFRvIiwicXVhbGl0eUNvbnRyb2xBZGRpdGlvbmFsT3B0aW9uc0Zyb20iLCJxdWFsaXR5Q29udHJvbEFkZGl0aW9uYWxPcHRpb25zVG8iLCJxdWFsaXR5Q29udHJvbENvbW1lbnRGcm9tIiwicXVhbGl0eUNvbnRyb2xDb21tZW50VG8iLCJxdWFsaXR5Q29udHJvbFZhbHVlRnJvbSIsInF1YWxpdHlDb250cm9sVmFsdWVUbyIsInJlbGF0ZWREaXNwbGF5TmFtZUZyb20iLCJyZWxhdGVkRGlzcGxheU5hbWVUbyIsInJlbGF0ZWRJZEZyb20iLCJyZWxhdGVkSWRUbyIsInJldmlld0NvbW1lbnRGcm9tIiwicmV2aWV3Q29tbWVudFRvIiwicmV2aWV3VmFsdWVGcm9tIiwicmV2aWV3VmFsdWVUbyIsInNlY3Rpb25OYW1lRnJvbSIsInNlY3Rpb25OYW1lVG8iLCJzZWN0aW9uVHlwZUZyb20iLCJzZWN0aW9uVHlwZVRvIiwic2VuZGVyIiwic2VudFRvQXV0aG9yaXRpZXNBdEZyb20iLCJzZW50VG9BdXRob3JpdGllc0F0VG8iLCJzb3VyY2VEaXNwbGF5TmFtZUZyb20iLCJzb3VyY2VEaXNwbGF5TmFtZVRvIiwic291cmNlSWRGcm9tIiwic291cmNlSWRUbyIsInNvdXJjZU1ldGFGcm9tIiwic291cmNlTWV0YVRvIiwic3RhdHVzRGlzcGxheU5hbWVGcm9tIiwic3RhdHVzRGlzcGxheU5hbWVUbyIsInN0YXR1c0lkRnJvbSIsInN0YXR1c0lkVG8iLCJzdGF0dXNSZWFzb25Gcm9tIiwic3RhdHVzUmVhc29uVG8iLCJzdGF0dXNSZW9wZW5lZENvdW50ZXJGcm9tIiwic3RhdHVzUmVvcGVuZWRDb3VudGVyVG8iLCJ0aWNrZXQiLCJ0aXRsZUZyb20iLCJ0aXRsZVRvIiwidW5pdE5hbWVGcm9tIiwidW5pdE5hbWVUbyIsInVuaXRUeXBlRnJvbSIsInVuaXRUeXBlVG8iLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJjbGllbnRFbWFpbEZyb20iLCJjbGllbnRFbWFpbFRvIiwiY2xpZW50TmFtZUZyb20iLCJjbGllbnROYW1lVG8iLCJjbGllbnRQaG9uZUZyb20iLCJjbGllbnRQaG9uZVRvIiwiZGV0YWlsc0Zyb20iLCJkZXRhaWxzVG8iLCJtZXRhRnJvbSIsIm1ldGFUbyIsInNvdXJjZU1ldGFGcm9tIiwic291cmNlTWV0YVRvIiwidGl0bGVGcm9tIiwidGl0bGVUbyJdfSwiVGlja2V0Q29tbWVudCI6eyJmaWVsZHMiOlsiY29udGVudCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsInNlbmRlciIsInRpY2tldCIsInR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiY29udGVudCJdfSwiVGlja2V0UGxhY2VDbGFzc2lmaWVyIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmFtZSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiVGlja2V0Q2F0ZWdvcnlDbGFzc2lmaWVyIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmFtZSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiVGlja2V0UHJvYmxlbUNsYXNzaWZpZXIiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJUaWNrZXRDbGFzc2lmaWVyIjp7ImZpZWxkcyI6WyJjYXRlZ29yeSIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInBsYWNlIiwicHJvYmxlbSIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlRpY2tldENvbW1lbnRGaWxlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImZpbGUiLCJpZCIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwidGlja2V0IiwidGlja2V0Q29tbWVudCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImZpbGUiXX0sIlVzZXJUaWNrZXRDb21tZW50UmVhZFRpbWUiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsInJlYWRDb21tZW50QXQiLCJyZWFkT3JnYW5pemF0aW9uQ29tbWVudEF0IiwicmVhZFJlc2lkZW50Q29tbWVudEF0Iiwic2VuZGVyIiwidGlja2V0IiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlRpY2tldFByb3BlcnR5SGludCI6eyJmaWVsZHMiOlsiY29udGVudCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJUaWNrZXRQcm9wZXJ0eUhpbnRQcm9wZXJ0eSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwicHJvcGVydHkiLCJzZW5kZXIiLCJ0aWNrZXRQcm9wZXJ0eUhpbnQiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJUaWNrZXRFeHBvcnRUYXNrIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImV4cG9ydGVkUmVjb3Jkc0NvdW50IiwiZmlsZSIsImZvcm1hdCIsImlkIiwibG9jYWxlIiwibWV0YSIsIm5ld0lkIiwib3B0aW9ucyIsInNlbmRlciIsInNvcnRCeSIsInN0YXR1cyIsInRpbWVab25lIiwidG90YWxSZWNvcmRzQ291bnQiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiIsIndoZXJlIl0sInNlbnNpdGl2ZUZpZWxkcyI6WyJmaWxlIiwibWV0YSIsIndoZXJlIl19LCJUaWNrZXRPcmdhbml6YXRpb25TZXR0aW5nIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWZhdWx0RGVhZGxpbmVEdXJhdGlvbiIsImRlbGV0ZWRBdCIsImR2IiwiZW1lcmdlbmN5RGVhZGxpbmVEdXJhdGlvbiIsImlkIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJwYWlkRGVhZGxpbmVEdXJhdGlvbiIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiLCJ3YXJyYW50eURlYWRsaW5lRHVyYXRpb24iXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkluY2lkZW50Ijp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkZXRhaWxzIiwiZHYiLCJoYXNBbGxQcm9wZXJ0aWVzIiwiaWQiLCJuZXdJZCIsIm51bWJlciIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInN0YXR1cyIsInRleHRGb3JSZXNpZGVudCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiLCJ3b3JrRmluaXNoIiwid29ya1N0YXJ0Iiwid29ya1R5cGUiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkluY2lkZW50Q2hhbmdlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZXRhaWxzRnJvbSIsImRldGFpbHNUbyIsImR2IiwiaWQiLCJpbmNpZGVudCIsIm9yZ2FuaXphdGlvbkRpc3BsYXlOYW1lRnJvbSIsIm9yZ2FuaXphdGlvbkRpc3BsYXlOYW1lVG8iLCJvcmdhbml6YXRpb25JZEZyb20iLCJvcmdhbml6YXRpb25JZFRvIiwic2VuZGVyIiwic3RhdHVzRnJvbSIsInN0YXR1c1RvIiwidGV4dEZvclJlc2lkZW50RnJvbSIsInRleHRGb3JSZXNpZGVudFRvIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiIsIndvcmtGaW5pc2hGcm9tIiwid29ya0ZpbmlzaFRvIiwid29ya1N0YXJ0RnJvbSIsIndvcmtTdGFydFRvIiwid29ya1R5cGVGcm9tIiwid29ya1R5cGVUbyJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiSW5jaWRlbnRQcm9wZXJ0eSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImluY2lkZW50IiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJwcm9wZXJ0eSIsInByb3BlcnR5QWRkcmVzcyIsInByb3BlcnR5QWRkcmVzc01ldGEiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJJbmNpZGVudENsYXNzaWZpZXIiOnsiZmllbGRzIjpbImNhdGVnb3J5IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwicHJvYmxlbSIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkluY2lkZW50Q2xhc3NpZmllckluY2lkZW50Ijp7ImZpZWxkcyI6WyJjbGFzc2lmaWVyIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImluY2lkZW50IiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJVc2VyRmF2b3JpdGVUaWNrZXQiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInRpY2tldCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJJbmNpZGVudEV4cG9ydFRhc2siOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZXhwb3J0ZWRSZWNvcmRzQ291bnQiLCJmaWxlIiwiZm9ybWF0IiwiaWQiLCJsb2NhbGUiLCJtZXRhIiwibmV3SWQiLCJzZW5kZXIiLCJzb3J0QnkiLCJzdGF0dXMiLCJ0aW1lWm9uZSIsInRvdGFsUmVjb3Jkc0NvdW50IiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiLCJ3aGVyZSJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZmlsZSIsIm1ldGEiLCJ3aGVyZSJdfSwiQ2FsbFJlY29yZCI6eyJmaWVsZHMiOlsiY2FsbGVyUGhvbmUiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkZXN0Q2FsbGVyUGhvbmUiLCJkdiIsImZpbGUiLCJpZCIsImltcG9ydElkIiwiaXNJbmNvbWluZ0NhbGwiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInN0YXJ0ZWRBdCIsInRhbGtUaW1lIiwidHJhbnNjcmlwdCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImNhbGxlclBob25lIiwiZGVzdENhbGxlclBob25lIiwiZmlsZSIsInRyYW5zY3JpcHQiXX0sIkNhbGxSZWNvcmRGcmFnbWVudCI6eyJmaWVsZHMiOlsiY2FsbFJlY29yZCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInN0YXJ0ZWRBdCIsInRpY2tldCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlRpY2tldEF1dG9Bc3NpZ25tZW50Ijp7ImZpZWxkcyI6WyJhc3NpZ25lZSIsImNsYXNzaWZpZXIiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImV4ZWN1dG9yIiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlRpY2tldERvY3VtZW50R2VuZXJhdGlvblRhc2siOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImRvY3VtZW50VHlwZSIsImR2IiwiZmlsZSIsImZvcm1hdCIsImlkIiwibWV0YSIsIm5ld0lkIiwicHJvZ3Jlc3MiLCJzZW5kZXIiLCJzdGF0dXMiLCJ0aWNrZXQiLCJ0aW1lWm9uZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJmaWxlIiwibWV0YSJdfSwiVGlja2V0T2JzZXJ2ZXIiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsInNlbmRlciIsInRpY2tldCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJNZXNzYWdlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkZWxpdmVyZWRBdCIsImR2IiwiZW1haWwiLCJlbWFpbEZyb20iLCJpZCIsImxhbmciLCJtZXRhIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJwaG9uZSIsInByb2Nlc3NpbmdNZXRhIiwicmVhZEF0IiwicmVtb3RlQ2xpZW50Iiwic2VuZGVyIiwic2VudEF0Iiwic3RhdHVzIiwidHlwZSIsInVuaXFLZXkiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZW1haWwiLCJlbWFpbEZyb20iLCJtZXRhIiwicGhvbmUiLCJwcm9jZXNzaW5nTWV0YSJdfSwiUmVtb3RlQ2xpZW50Ijp7ImZpZWxkcyI6WyJhcHBJZCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImRldmljZUlkIiwiZGV2aWNlS2V5IiwiZGV2aWNlUGxhdGZvcm0iLCJkdiIsImlkIiwibWV0YSIsIm5ld0lkIiwib3duZXIiLCJwdXNoVG9rZW4iLCJwdXNoVG9rZW5Wb0lQIiwicHVzaFRyYW5zcG9ydCIsInB1c2hUcmFuc3BvcnRWb0lQIiwicHVzaFR5cGUiLCJwdXNoVHlwZVZvSVAiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJkZXZpY2VLZXkiLCJwdXNoVG9rZW4iLCJwdXNoVG9rZW5Wb0lQIl19LCJNZXNzYWdlVXNlckJsYWNrTGlzdCI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZGVzY3JpcHRpb24iLCJkdiIsImVtYWlsIiwiaWQiLCJuZXdJZCIsInBob25lIiwic2VuZGVyIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJlbWFpbCIsInBob25lIl19LCJNZXNzYWdlT3JnYW5pemF0aW9uQmxhY2tMaXN0Ijp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkZXNjcmlwdGlvbiIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJOb3RpZmljYXRpb25Vc2VyU2V0dGluZyI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImlzRW5hYmxlZCIsIm1lc3NhZ2VUcmFuc3BvcnQiLCJtZXNzYWdlVHlwZSIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlRlbGVncmFtVXNlckNoYXQiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsInNlbmRlciIsInRlbGVncmFtQ2hhdElkIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlJlbW90ZUNsaWVudFB1c2hUb2tlbiI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImlzUHVzaCIsImlzVm9JUCIsIm5ld0lkIiwicHJvdmlkZXIiLCJyZW1vdGVDbGllbnQiLCJzZW5kZXIiLCJ0b2tlbiIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbInRva2VuIl19LCJDb250YWN0Ijp7ImZpZWxkcyI6WyJjb21tdW5pdHlGZWUiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImVtYWlsIiwiaWQiLCJpc1ZlcmlmaWVkIiwibWV0YSIsIm5hbWUiLCJuZXdJZCIsIm5vdGUiLCJvcmdhbml6YXRpb24iLCJvd25lcnNoaXBQZXJjZW50YWdlIiwicGhvbmUiLCJwcm9wZXJ0eSIsInJvbGUiLCJzZW5kZXIiLCJ1bml0TmFtZSIsInVuaXRUeXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZW1haWwiLCJuYW1lIiwibm90ZSIsInBob25lIl19LCJDb250YWN0Um9sZSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5hbWUiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkNvbnRhY3RFeHBvcnRUYXNrIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImV4cG9ydGVkUmVjb3Jkc0NvdW50IiwiZmlsZSIsImZvcm1hdCIsImlkIiwibG9jYWxlIiwibWV0YSIsIm5ld0lkIiwic2VuZGVyIiwic29ydEJ5Iiwic3RhdHVzIiwidGltZVpvbmUiLCJ0b3RhbFJlY29yZHNDb3VudCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Iiwid2hlcmUiXSwic2Vuc2l0aXZlRmllbGRzIjpbImZpbGUiLCJtZXRhIiwid2hlcmUiXX0sIlJlc2lkZW50Ijp7ImZpZWxkcyI6WyJhZGRyZXNzIiwiYWRkcmVzc0tleSIsImFkZHJlc3NNZXRhIiwiYWRkcmVzc1NvdXJjZXMiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJwcm9wZXJ0eSIsInNlbmRlciIsInVuaXROYW1lIiwidW5pdFR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiU2VydmljZUNvbnN1bWVyIjp7ImZpZWxkcyI6WyJhY2NvdW50TnVtYmVyIiwiYWNxdWlyaW5nSW50ZWdyYXRpb25Db250ZXh0IiwiYmlsbGluZ0ludGVncmF0aW9uQ29udGV4dCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJpc0Rpc2NvdmVyZWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInBheW1lbnRDYXRlZ29yeSIsInJlc2lkZW50Iiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiVG91clN0ZXAiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZGVyIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwic3RhdHVzIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlVzZXJIZWxwUmVxdWVzdCI6eyJmaWVsZHMiOlsiYmlsbGluZ0ludGVncmF0aW9uIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJlbWFpbCIsImlkIiwiaXNSZWFkeVRvU2VuZCIsIm1ldGEiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInBob25lIiwic2VuZGVyIiwic3Vic2NyaXB0aW9uUGxhblByaWNpbmdSdWxlIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImVtYWlsIiwibWV0YSIsInBob25lIl19LCJVc2VySGVscFJlcXVlc3RGaWxlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImZpbGUiLCJpZCIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlckhlbHBSZXF1ZXN0IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZmlsZSJdfSwiTWV0ZXJSZXNvdXJjZSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm1lYXN1cmUiLCJuYW1lIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJNZXRlclJlYWRpbmdTb3VyY2UiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJzZW5kZXIiLCJ0eXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiTWV0ZXJSZWFkaW5nIjp7ImZpZWxkcyI6WyJhY2NvdW50TnVtYmVyIiwiYmlsbGluZ1N0YXR1cyIsImJpbGxpbmdTdGF0dXNUZXh0IiwiY2xpZW50IiwiY2xpZW50RW1haWwiLCJjbGllbnROYW1lIiwiY2xpZW50UGhvbmUiLCJjb250YWN0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGF0ZSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJtZXRlciIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwic291cmNlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiIsInZhbHVlMSIsInZhbHVlMiIsInZhbHVlMyIsInZhbHVlNCJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiY2xpZW50RW1haWwiLCJjbGllbnROYW1lIiwiY2xpZW50UGhvbmUiXX0sIk1ldGVyIjp7ImZpZWxkcyI6WyJhY2NvdW50TnVtYmVyIiwiYXJjaGl2ZURhdGUiLCJiMmJBcHAiLCJiMmNBcHAiLCJjb21taXNzaW9uaW5nRGF0ZSIsImNvbnRyb2xSZWFkaW5nc0RhdGUiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaW5zdGFsbGF0aW9uRGF0ZSIsImlzQXV0b21hdGljIiwibWV0YSIsIm5ld0lkIiwibmV4dFZlcmlmaWNhdGlvbkRhdGUiLCJudW1iZXIiLCJudW1iZXJPZlRhcmlmZnMiLCJvcmdhbml6YXRpb24iLCJwbGFjZSIsInByb3BlcnR5IiwicmVzb3VyY2UiLCJzZWFsaW5nRGF0ZSIsInNlbmRlciIsInVuaXROYW1lIiwidW5pdFR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2IiwidmVyaWZpY2F0aW9uRGF0ZSJdLCJzZW5zaXRpdmVGaWVsZHMiOlsibWV0YSJdfSwiUHJvcGVydHlNZXRlciI6eyJmaWVsZHMiOlsiYXJjaGl2ZURhdGUiLCJiMmJBcHAiLCJjb21taXNzaW9uaW5nRGF0ZSIsImNvbnRyb2xSZWFkaW5nc0RhdGUiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaW5zdGFsbGF0aW9uRGF0ZSIsImlzQXV0b21hdGljIiwibWV0YSIsIm5ld0lkIiwibmV4dFZlcmlmaWNhdGlvbkRhdGUiLCJudW1iZXIiLCJudW1iZXJPZlRhcmlmZnMiLCJvcmdhbml6YXRpb24iLCJwcm9wZXJ0eSIsInJlc291cmNlIiwic2VhbGluZ0RhdGUiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2IiwidmVyaWZpY2F0aW9uRGF0ZSJdLCJzZW5zaXRpdmVGaWVsZHMiOlsibWV0YSJdfSwiUHJvcGVydHlNZXRlclJlYWRpbmciOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRhdGUiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibWV0ZXIiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInNvdXJjZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiLCJ2YWx1ZTEiLCJ2YWx1ZTIiLCJ2YWx1ZTMiLCJ2YWx1ZTQiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIk1ldGVyUmVwb3J0aW5nUGVyaW9kIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmV3SWQiLCJub3RpZnlFbmREYXkiLCJub3RpZnlTdGFydERheSIsIm9yZ2FuaXphdGlvbiIsInByb3BlcnR5IiwicmVzdHJpY3Rpb25FbmREYXkiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJNZXRlclJlc291cmNlT3duZXIiOnsiZmllbGRzIjpbImFkZHJlc3MiLCJhZGRyZXNzS2V5IiwiYWRkcmVzc01ldGEiLCJhZGRyZXNzU291cmNlcyIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInJlc291cmNlIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiTWV0ZXJSZWFkaW5nc0ltcG9ydFRhc2siOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZXJyb3JGaWxlIiwiZXJyb3JNZXNzYWdlIiwiZmlsZSIsImZvcm1hdCIsImlkIiwiaW1wb3J0ZWRSZWNvcmRzQ291bnQiLCJpc1Byb3BlcnR5TWV0ZXJzIiwibG9jYWxlIiwibWV0YSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwicHJvY2Vzc2VkUmVjb3Jkc0NvdW50Iiwic2VuZGVyIiwic3RhdHVzIiwidG90YWxSZWNvcmRzQ291bnQiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZXJyb3JGaWxlIiwiZmlsZSIsIm1ldGEiXX0sIk1ldGVyUmVhZGluZ0V4cG9ydFRhc2siOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZXhwb3J0ZWRSZWNvcmRzQ291bnQiLCJmaWxlIiwiZm9ybWF0IiwiaWQiLCJsb2NhbGUiLCJtZXRhIiwibmV3SWQiLCJzZW5kZXIiLCJzb3J0QnkiLCJzdGF0dXMiLCJ0aW1lWm9uZSIsInRvdGFsUmVjb3Jkc0NvdW50IiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiLCJ3aGVyZSJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZmlsZSIsIm1ldGEiLCJ3aGVyZSJdfSwiTWV0ZXJVc2VyU2V0dGluZyI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm1ldGVyIiwibmFtZSIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlN1YnNjcmlwdGlvblBsYW4iOnsiZmllbGRzIjpbImFpIiwiYW5hbHl0aWNzIiwiY2FuQmVQcm9tb3RlZCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1c3RvbWl6YXRpb24iLCJkZWxldGVkQXQiLCJkZXNjcmlwdGlvbiIsImR2IiwiZW5hYmxlZEIyQkFwcHMiLCJlbmFibGVkQjJDQXBwcyIsImlkIiwiaXNIaWRkZW4iLCJtYXJrZXRwbGFjZSIsIm1ldGVycyIsIm5hbWUiLCJuZXdJZCIsIm5ld3MiLCJvcmdhbml6YXRpb25UeXBlIiwicGF5bWVudHMiLCJwcmlvcml0eSIsInByb3BlcnRpZXMiLCJzZW5kZXIiLCJzdXBwb3J0IiwidGlja2V0cyIsInRyaWFsRGF5cyIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlN1YnNjcmlwdGlvblBsYW5QcmljaW5nUnVsZSI6eyJmaWVsZHMiOlsiY29uZGl0aW9ucyIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1cnJlbmN5Q29kZSIsImRlbGV0ZWRBdCIsImRlc2NyaXB0aW9uIiwiZHYiLCJpZCIsImlzSGlkZGVuIiwibmFtZSIsIm5ld0lkIiwicGVyaW9kIiwicHJpY2UiLCJwcmlvcml0eSIsInNlbmRlciIsInN1YnNjcmlwdGlvblBsYW4iLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJTdWJzY3JpcHRpb25Db250ZXh0Ijp7ImZpZWxkcyI6WyJiaW5kaW5nSWQiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImVuZEF0IiwiZnJvemVuUGF5bWVudEluZm8iLCJpZCIsImludm9pY2UiLCJpc1RyaWFsIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJzdGFydEF0Iiwic3RhdHVzIiwic3Vic2NyaXB0aW9uUGxhbiIsInN1YnNjcmlwdGlvblBsYW5QcmljaW5nUnVsZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImJpbmRpbmdJZCIsImZyb3plblBheW1lbnRJbmZvIl19LCJBY3F1aXJpbmdJbnRlZ3JhdGlvbiI6eyJmaWVsZHMiOlsiY2FuR3JvdXBSZWNlaXB0cyIsImNvbnRleHREZWZhdWx0U3RhdHVzIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJleHBsaWNpdEZlZURpc3RyaWJ1dGlvblNjaGVtYSIsImhvc3RVcmwiLCJpZCIsImlzSGlkZGVuIiwibWF4aW11bVBheW1lbnRBbW91bnQiLCJtaW5pbXVtUGF5bWVudEFtb3VudCIsIm5hbWUiLCJuZXdJZCIsInNlbmRlciIsInNldHVwVXJsIiwic3VwcG9ydGVkQmlsbGluZ0ludGVncmF0aW9uc0dyb3VwIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiLCJ2YXRQZXJjZW50T3B0aW9ucyJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQWNxdWlyaW5nSW50ZWdyYXRpb25BY2Nlc3NSaWdodCI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImludGVncmF0aW9uIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQWNxdWlyaW5nSW50ZWdyYXRpb25Db250ZXh0Ijp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImVtYWlsIiwiaWQiLCJpbXBsaWNpdEZlZURpc3RyaWJ1dGlvblNjaGVtYSIsImludGVncmF0aW9uIiwiaW52b2ljZUVtYWlscyIsImludm9pY2VJbXBsaWNpdEZlZURpc3RyaWJ1dGlvblNjaGVtYSIsImludm9pY2VSZWFzb24iLCJpbnZvaWNlUmVjaXBpZW50IiwiaW52b2ljZVNhbGVzVGF4UGVyY2VudCIsImludm9pY2VTdGF0dXMiLCJpbnZvaWNlVGF4UmVnaW1lIiwiaW52b2ljZVZhdFBlcmNlbnQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInJlYXNvbiIsInJlY2lwaWVudCIsInNlbmRlciIsInNldHRpbmdzIiwic3RhdGUiLCJzdGF0dXMiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJlbWFpbCIsImludm9pY2VFbWFpbHMiLCJzZXR0aW5ncyIsInN0YXRlIl19LCJNdWx0aVBheW1lbnQiOnsiZmllbGRzIjpbImFtb3VudFdpdGhvdXRFeHBsaWNpdEZlZSIsImNhcmROdW1iZXIiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJjdXJyZW5jeUNvZGUiLCJkZWxldGVkQXQiLCJkdiIsImV4cGxpY2l0RmVlIiwiZXhwbGljaXRTZXJ2aWNlQ2hhcmdlIiwiaWQiLCJpbXBsaWNpdEZlZSIsImltcG9ydElkIiwiaW50ZWdyYXRpb24iLCJtZXRhIiwibmV3SWQiLCJwYXllckVtYWlsIiwicGF5bWVudFdheSIsInJlY3VycmVudFBheW1lbnRDb250ZXh0Iiwic2VuZGVyIiwic2VydmljZUNhdGVnb3J5Iiwic2VydmljZUZlZSIsInN0YXR1cyIsInRyYW5zYWN0aW9uSWQiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiIsIndpdGhkcmF3bkF0Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJtZXRhIiwicGF5ZXJFbWFpbCJdfSwiUGF5bWVudCI6eyJmaWVsZHMiOlsiYWNjb3VudE51bWJlciIsImFkdmFuY2VkQXQiLCJhbW91bnQiLCJjb250ZXh0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiY3VycmVuY3lDb2RlIiwiZGVsZXRlZEF0IiwiZGVwb3NpdGVkRGF0ZSIsImR2IiwiZXhwbGljaXRGZWUiLCJleHBsaWNpdFNlcnZpY2VDaGFyZ2UiLCJmcm96ZW5EaXN0cmlidXRpb24iLCJmcm96ZW5JbnZvaWNlIiwiZnJvemVuUmVjZWlwdCIsImZyb3plblNwbGl0cyIsImlkIiwiaW1wbGljaXRGZWUiLCJpbXBvcnRJZCIsImludm9pY2UiLCJtdWx0aVBheW1lbnQiLCJuZXdJZCIsIm9yZGVyIiwib3JnYW5pemF0aW9uIiwicGVyaW9kIiwicG9zUmVjZWlwdFVybCIsInB1cnBvc2UiLCJyYXdBZGRyZXNzIiwicmVjZWlwdCIsInJlY2lwaWVudCIsInJlY2lwaWVudEJhbmtBY2NvdW50IiwicmVjaXBpZW50QmljIiwic2VuZGVyIiwic2VydmljZUZlZSIsInN0YXR1cyIsInRyYW5zZmVyRGF0ZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImZyb3plbkRpc3RyaWJ1dGlvbiIsImZyb3plbkludm9pY2UiLCJmcm96ZW5SZWNlaXB0IiwiZnJvemVuU3BsaXRzIiwicG9zUmVjZWlwdFVybCJdfSwiUGF5bWVudFN0YXR1c0NoYW5nZVdlYmhvb2tVcmwiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImRlc2NyaXB0aW9uIiwiZHYiLCJpZCIsImlzRW5hYmxlZCIsIm5hbWUiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVybCIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlJlY3VycmVudFBheW1lbnRDb250ZXh0Ijp7ImZpZWxkcyI6WyJhdXRvUGF5UmVjZWlwdHMiLCJiaWxsaW5nQ2F0ZWdvcnkiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImVuYWJsZWQiLCJpZCIsImxpbWl0IiwibmV3SWQiLCJwYXltZW50RGF5Iiwic2VuZGVyIiwic2VydmljZUNvbnN1bWVyIiwic2V0dGluZ3MiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJzZXR0aW5ncyJdfSwiUHJvcGVydHlTY29wZSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJoYXNBbGxFbXBsb3llZXMiLCJoYXNBbGxQcm9wZXJ0aWVzIiwiaWQiLCJuYW1lIiwibmV3SWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJQcm9wZXJ0eVNjb3BlT3JnYW5pemF0aW9uRW1wbG95ZWUiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZW1wbG95ZWUiLCJpZCIsIm5ld0lkIiwicHJvcGVydHlTY29wZSIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIlByb3BlcnR5U2NvcGVQcm9wZXJ0eSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwicHJvcGVydHkiLCJwcm9wZXJ0eVNjb3BlIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiTmV3c0l0ZW0iOnsiZmllbGRzIjpbImJvZHkiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaXNQdWJsaXNoZWQiLCJuZXdJZCIsIm51bWJlciIsIm9yZ2FuaXphdGlvbiIsInB1Ymxpc2hlZEF0Iiwic2VuZEF0Iiwic2VuZGVyIiwic2VudEF0IiwidGl0bGUiLCJ0eXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiIsInZhbGlkQmVmb3JlIl0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJOZXdzSXRlbVNjb3BlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmV3SWQiLCJuZXdzSXRlbSIsInByb3BlcnR5Iiwic2VuZGVyIiwidHlwZSIsInVuaXROYW1lIiwidW5pdFR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJOZXdzSXRlbVRlbXBsYXRlIjp7ImZpZWxkcyI6WyJib2R5IiwiY2F0ZWdvcnkiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmFtZSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwidGl0bGUiLCJ0eXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiTmV3c0l0ZW1Vc2VyUmVhZCI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwibmV3c0l0ZW0iLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiTmV3c0l0ZW1SZWNpcGllbnRzRXhwb3J0VGFzayI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJmaWxlIiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInNjb3BlcyIsInNlbmRlciIsInN0YXR1cyIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInVzZXIiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJmaWxlIl19LCJOZXdzSXRlbVNoYXJpbmciOnsiZmllbGRzIjpbImIyYkFwcENvbnRleHQiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibGFzdFBvc3RSZXF1ZXN0IiwibmV3SWQiLCJuZXdzSXRlbSIsInNlbmRlciIsInNoYXJpbmdQYXJhbXMiLCJzdGF0dXMiLCJzdGF0dXNNZXNzYWdlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsibGFzdFBvc3RSZXF1ZXN0Iiwic2hhcmluZ1BhcmFtcyJdfSwiTmV3c0l0ZW1GaWxlIjp7ImZpZWxkcyI6WyJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImZpbGUiLCJpZCIsIm5ld0lkIiwibmV3c0l0ZW0iLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJCMkJBcHAiOnsiZmllbGRzIjpbImFkZGl0aW9uYWxEb21haW5zIiwiYXBwVXJsIiwiY2F0ZWdvcnkiLCJjb250ZXh0RGVmYXVsdFN0YXR1cyIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImRldGFpbGVkRGVzY3JpcHRpb24iLCJkZXZlbG9wZXIiLCJkZXZlbG9wZXJVcmwiLCJkaXNwbGF5UHJpb3JpdHkiLCJkdiIsImZlYXR1cmVzIiwiZ2FsbGVyeSIsImhhc0R5bmFtaWNUaXRsZSIsImljb24iLCJpZCIsImltcG9ydElkIiwiaW1wb3J0UmVtb3RlU3lzdGVtIiwiaXNHbG9iYWwiLCJpc0hpZGRlbiIsImlzUHVibGljIiwibGFiZWwiLCJsb2dvIiwibWVudUNhdGVnb3J5IiwibmFtZSIsIm5ld0lkIiwibmV3c1NoYXJpbmdDb25maWciLCJvaWRjQ2xpZW50IiwicG9zSW50ZWdyYXRpb25Db25maWciLCJwcmljZSIsInNlbmRlciIsInNob3J0RGVzY3JpcHRpb24iLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJCMkJBcHBDb250ZXh0Ijp7ImZpZWxkcyI6WyJhcHAiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibWV0YSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwic3RhdHVzIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsibWV0YSJdfSwiQjJCQXBwQWNjZXNzUmlnaHQiOnsiZmllbGRzIjpbImFjY2Vzc1JpZ2h0U2V0IiwiYXBwIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkIyQ0FwcCI6eyJmaWVsZHMiOlsiYWRkaXRpb25hbERvbWFpbnMiLCJhcHBVcmwiLCJjb2xvclNjaGVtYSIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1cnJlbnRCdWlsZCIsImRlbGV0ZWRBdCIsImRldmVsb3BlciIsImR2IiwiaWQiLCJpbXBvcnRJZCIsImltcG9ydFJlbW90ZVN5c3RlbSIsImlzSGlkZGVuIiwibG9nbyIsIm5hbWUiLCJuZXdJZCIsIm9pZGNDbGllbnQiLCJzZW5kZXIiLCJzaG9ydERlc2NyaXB0aW9uIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQjJDQXBwQWNjZXNzUmlnaHQiOnsiZmllbGRzIjpbImFjY2Vzc1JpZ2h0U2V0IiwiYXBwIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImltcG9ydElkIiwiaW1wb3J0UmVtb3RlU3lzdGVtIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQjJDQXBwQnVpbGQiOnsiZmllbGRzIjpbImFwcCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRhdGEiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwiaW1wb3J0SWQiLCJpbXBvcnRSZW1vdGVTeXN0ZW0iLCJuZXdJZCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiLCJ2ZXJzaW9uIl0sInNlbnNpdGl2ZUZpZWxkcyI6WyJkYXRhIl19LCJCMkNBcHBQcm9wZXJ0eSI6eyJmaWVsZHMiOlsiYWRkcmVzcyIsImFkZHJlc3NLZXkiLCJhZGRyZXNzTWV0YSIsImFkZHJlc3NTb3VyY2VzIiwiYXBwIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQjJCQXBwUHJvbW9CbG9jayI6eyJmaWVsZHMiOlsiYmFja2dyb3VuZENvbG9yIiwiYmFja2dyb3VuZEltYWdlIiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJleHRlcm5hbCIsImlkIiwibmV3SWQiLCJwcmlvcml0eSIsInNlbmRlciIsInN1YnRpdGxlIiwidGFyZ2V0VXJsIiwidGV4dFZhcmlhbnQiLCJ0aXRsZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkIyQkFwcFBlcm1pc3Npb24iOnsiZmllbGRzIjpbImFwcCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJrZXkiLCJuYW1lIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJCMkJBcHBSb2xlIjp7ImZpZWxkcyI6WyJhcHAiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibmV3SWQiLCJwZXJtaXNzaW9ucyIsInJvbGUiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJCMkJBcHBBY2Nlc3NSaWdodFNldCI6eyJmaWVsZHMiOlsiYXBwIiwiY2FuRXhlY3V0ZVJlZ2lzdGVyQmlsbGluZ1JlY2VpcHRGaWxlIiwiY2FuRXhlY3V0ZVJlZ2lzdGVyQmlsbGluZ1JlY2VpcHRzIiwiY2FuRXhlY3V0ZVJlZ2lzdGVyTWV0ZXJzUmVhZGluZ3MiLCJjYW5FeGVjdXRlUmVnaXN0ZXJQcm9wZXJ0eU1ldGVyc1JlYWRpbmdzIiwiY2FuRXhlY3V0ZVNlbmRCMkJBcHBQdXNoTWVzc2FnZSIsImNhbkV4ZWN1dGVTZXRQYXltZW50UG9zUmVjZWlwdFVybCIsImNhbk1hbmFnZUIyQkFjY2Vzc1Rva2VucyIsImNhbk1hbmFnZUJpbGxpbmdBY2NvdW50cyIsImNhbk1hbmFnZUJpbGxpbmdJbnRlZ3JhdGlvbk9yZ2FuaXphdGlvbkNvbnRleHRzIiwiY2FuTWFuYWdlQmlsbGluZ1Byb3BlcnRpZXMiLCJjYW5NYW5hZ2VCaWxsaW5nUmVjZWlwdEZpbGVzIiwiY2FuTWFuYWdlQmlsbGluZ1JlY2VpcHRzIiwiY2FuTWFuYWdlQmlsbGluZ1JlY2lwaWVudHMiLCJjYW5NYW5hZ2VDb250YWN0cyIsImNhbk1hbmFnZUN1c3RvbVZhbHVlcyIsImNhbk1hbmFnZUludm9pY2VzIiwiY2FuTWFuYWdlTWV0ZXJSZWFkaW5ncyIsImNhbk1hbmFnZU1ldGVyUmVwb3J0aW5nUGVyaW9kcyIsImNhbk1hbmFnZU1ldGVycyIsImNhbk1hbmFnZU9yZ2FuaXphdGlvbkVtcGxveWVlUm9sZXMiLCJjYW5NYW5hZ2VPcmdhbml6YXRpb25FbXBsb3llZXMiLCJjYW5NYW5hZ2VPcmdhbml6YXRpb25zIiwiY2FuTWFuYWdlUGF5bWVudHMiLCJjYW5NYW5hZ2VQcm9wZXJ0aWVzIiwiY2FuTWFuYWdlVGlja2V0Q29tbWVudEZpbGVzIiwiY2FuTWFuYWdlVGlja2V0Q29tbWVudHMiLCJjYW5NYW5hZ2VUaWNrZXRGaWxlcyIsImNhbk1hbmFnZVRpY2tldHMiLCJjYW5SZWFkQjJCQWNjZXNzVG9rZW5zIiwiY2FuUmVhZEJpbGxpbmdBY2NvdW50cyIsImNhblJlYWRCaWxsaW5nSW50ZWdyYXRpb25Pcmdhbml6YXRpb25Db250ZXh0cyIsImNhblJlYWRCaWxsaW5nUHJvcGVydGllcyIsImNhblJlYWRCaWxsaW5nUmVjZWlwdEZpbGVzIiwiY2FuUmVhZEJpbGxpbmdSZWNlaXB0cyIsImNhblJlYWRCaWxsaW5nUmVjaXBpZW50cyIsImNhblJlYWRDb250YWN0cyIsImNhblJlYWRDdXN0b21WYWx1ZXMiLCJjYW5SZWFkSW52b2ljZXMiLCJjYW5SZWFkTWV0ZXJSZWFkaW5ncyIsImNhblJlYWRNZXRlclJlcG9ydGluZ1BlcmlvZHMiLCJjYW5SZWFkTWV0ZXJzIiwiY2FuUmVhZE9yZ2FuaXphdGlvbkVtcGxveWVlUm9sZXMiLCJjYW5SZWFkT3JnYW5pemF0aW9uRW1wbG95ZWVzIiwiY2FuUmVhZE9yZ2FuaXphdGlvbnMiLCJjYW5SZWFkUGF5bWVudHMiLCJjYW5SZWFkUHJvcGVydGllcyIsImNhblJlYWRUaWNrZXRDb21tZW50RmlsZXMiLCJjYW5SZWFkVGlja2V0Q29tbWVudHMiLCJjYW5SZWFkVGlja2V0RmlsZXMiLCJjYW5SZWFkVGlja2V0cyIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJzZW5kZXIiLCJ0eXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQjJCQXBwTmV3c1NoYXJpbmdDb25maWciOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImN1c3RvbUZvcm1VcmwiLCJkZWxldGVkQXQiLCJkdiIsImdldFJlY2lwaWVudHNDb3VudGVyc1VybCIsImdldFJlY2lwaWVudHNVcmwiLCJpY29uIiwiaWQiLCJuYW1lIiwibmV3SWQiLCJwcmV2aWV3UGljdHVyZSIsInByZXZpZXdVcmwiLCJwdWJsaXNoVXJsIiwicHVzaE5vdGlmaWNhdGlvblNldHRpbmdzIiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiY3VzdG9tRm9ybVVybCIsImdldFJlY2lwaWVudHNDb3VudGVyc1VybCIsImdldFJlY2lwaWVudHNVcmwiLCJwcmV2aWV3VXJsIiwicHVibGlzaFVybCIsInB1c2hOb3RpZmljYXRpb25TZXR0aW5ncyJdfSwiQXBwTWVzc2FnZVNldHRpbmciOnsiZmllbGRzIjpbImIyYkFwcCIsImIyY0FwcCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm5vdGlmaWNhdGlvbldpbmRvd1NpemUiLCJudW1iZXJPZk5vdGlmaWNhdGlvbkluV2luZG93IiwicmVhc29uIiwic2VuZGVyIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkIyQkFjY2Vzc1Rva2VuIjp7ImZpZWxkcyI6WyJjb250ZXh0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJleHBpcmVzQXQiLCJpZCIsIm5ld0lkIiwicmlnaHRTZXQiLCJzZW5kZXIiLCJzZXNzaW9uSWQiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ1c2VyIiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsic2Vzc2lvbklkIl19LCJDdXN0b21GaWVsZCI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImlzVW5pcXVlUGVyT2JqZWN0IiwiaXNWaXNpYmxlIiwibG9jYWxlIiwibW9kZWxOYW1lIiwibmFtZSIsIm5ld0lkIiwicHJpb3JpdHkiLCJzZW5kZXIiLCJzdGFmZkNhblJlYWQiLCJ0eXBlIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiIsInZhbGlkYXRpb25SdWxlcyJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiQjJCQXBwUG9zSW50ZWdyYXRpb25Db25maWciOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZmV0Y2hMYXN0UG9zUmVjZWlwdFVybCIsImlkIiwibmV3SWQiLCJwYXltZW50c0FsZXJ0UGFnZVVybCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImZldGNoTGFzdFBvc1JlY2VpcHRVcmwiLCJwYXltZW50c0FsZXJ0UGFnZVVybCJdfSwiQjJDQXBwQWNjZXNzUmlnaHRTZXQiOnsiZmllbGRzIjpbImFwcCIsImNhbkV4ZWN1dGVTZW5kVm9JUFN0YXJ0TWVzc2FnZSIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIk1vYmlsZUZlYXR1cmVDb25maWciOnsiZmllbGRzIjpbImNvbW1vblBob25lIiwiY29udGVudENvbmZpZ3VyYXRpb24iLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJkZWxldGVkQXQiLCJkdiIsImlkIiwibWV0YSIsIm5ld0lkIiwib25seUdyZWF0ZXJUaGFuUHJldmlvdXNNZXRlclJlYWRpbmdJc0VuYWJsZWQiLCJvcmdhbml6YXRpb24iLCJzZW5kZXIiLCJ0aWNrZXRTdWJtaXR0aW5nSXNEaXNhYmxlZCIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImNvbW1vblBob25lIl19LCJNYXJrZXRDYXRlZ29yeSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsImltYWdlIiwibW9iaWxlU2V0dGluZ3MiLCJuYW1lIiwibmV3SWQiLCJvcmRlciIsInBhcmVudENhdGVnb3J5Iiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsibW9iaWxlU2V0dGluZ3MiXX0sIk1hcmtldEl0ZW0iOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImRlc2NyaXB0aW9uIiwiZHYiLCJpZCIsIm1hcmtldENhdGVnb3J5IiwibmFtZSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwic2VuZGVyIiwic2t1IiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOltdfSwiSW52b2ljZSI6eyJmaWVsZHMiOlsiYWNjb3VudE51bWJlciIsImFtb3VudERpc3RyaWJ1dGlvbiIsImNhbmNlbGVkQXQiLCJjbGllbnQiLCJjbGllbnROYW1lIiwiY2xpZW50UGhvbmUiLCJjb250YWN0IiwiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJpZCIsIm5ld0lkIiwibnVtYmVyIiwib3JnYW5pemF0aW9uIiwicGFpZEF0IiwicGF5ZXJPcmdhbml6YXRpb24iLCJwYXltZW50U3RhdHVzQ2hhbmdlV2ViaG9va1NlY3JldCIsInBheW1lbnRTdGF0dXNDaGFuZ2VXZWJob29rVXJsIiwicGF5bWVudFR5cGUiLCJwcm9wZXJ0eSIsInB1Ymxpc2hlZEF0Iiwicm93cyIsInNlbmRlciIsInN0YXR1cyIsInRpY2tldCIsInRvUGF5IiwidHlwZSIsInVuaXROYW1lIiwidW5pdFR5cGUiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6WyJhbW91bnREaXN0cmlidXRpb24iLCJjbGllbnROYW1lIiwiY2xpZW50UGhvbmUiLCJwYXltZW50U3RhdHVzQ2hhbmdlV2ViaG9va1NlY3JldCIsInBheW1lbnRTdGF0dXNDaGFuZ2VXZWJob29rVXJsIl19LCJNYXJrZXRJdGVtRmlsZSI6eyJmaWVsZHMiOlsiY3JlYXRlZEF0IiwiY3JlYXRlZEJ5IiwiZGVsZXRlZEF0IiwiZHYiLCJmaWxlIiwiaWQiLCJtYXJrZXRJdGVtIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJNYXJrZXRJdGVtUHJpY2UiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJtYXJrZXRJdGVtIiwibmV3SWQiLCJwcmljZSIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIk1hcmtldFByaWNlU2NvcGUiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJtYXJrZXRJdGVtUHJpY2UiLCJuZXdJZCIsInByb3BlcnR5Iiwic2VuZGVyIiwidHlwZSIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIk1hcmtldFNldHRpbmciOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInJlc2lkZW50QWxsb3dlZFBheW1lbnRUeXBlcyIsInNlbmRlciIsInVwZGF0ZWRBdCIsInVwZGF0ZWRCeSIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbXX0sIkRvY3VtZW50Q2F0ZWdvcnkiOnsiZmllbGRzIjpbImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiaWQiLCJuYW1lIiwibmV3SWQiLCJzZW5kZXIiLCJ1cGRhdGVkQXQiLCJ1cGRhdGVkQnkiLCJ2Il0sInNlbnNpdGl2ZUZpZWxkcyI6W119LCJEb2N1bWVudCI6eyJmaWVsZHMiOlsiY2FuUmVhZEJ5UmVzaWRlbnQiLCJjYXRlZ29yeSIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZmlsZSIsImlkIiwibWV0YSIsIm5hbWUiLCJuZXdJZCIsIm9yZ2FuaXphdGlvbiIsInByb3BlcnR5Iiwic2VuZGVyIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidiJdLCJzZW5zaXRpdmVGaWVsZHMiOlsiZmlsZSIsIm1ldGEiXX0sIkV4ZWN1dGlvbkFJRmxvd1Rhc2siOnsiZmllbGRzIjpbImFpU2Vzc2lvbklkIiwiY2xlYW5Db250ZXh0IiwiY29udGV4dCIsImNyZWF0ZWRBdCIsImNyZWF0ZWRCeSIsImRlbGV0ZWRBdCIsImR2IiwiZXJyb3IiLCJlcnJvck1lc3NhZ2UiLCJmbG93VHlwZSIsImlkIiwiaXRlbUlkIiwibG9jYWxlIiwibWV0YSIsIm1vZGVsTmFtZSIsIm5ld0lkIiwib3JnYW5pemF0aW9uIiwicmVzdWx0Iiwic2VuZGVyIiwic3RhdHVzIiwidXBkYXRlZEF0IiwidXBkYXRlZEJ5IiwidXNlciIsInYiXSwic2Vuc2l0aXZlRmllbGRzIjpbImNsZWFuQ29udGV4dCIsImNvbnRleHQiLCJtZXRhIiwicmVzdWx0Il19fX0= + +exports.up = async (knex) => { + await knex.raw(` + BEGIN; +-- +-- Add field canExecuteSendVoIPStartMessage to b2cappaccessrightset +-- +ALTER TABLE "B2CAppAccessRightSet" ADD COLUMN "canExecuteSendVoIPStartMessage" boolean DEFAULT false NOT NULL; +ALTER TABLE "B2CAppAccessRightSet" ALTER COLUMN "canExecuteSendVoIPStartMessage" DROP DEFAULT; +-- +-- Add field canExecuteSendVoIPStartMessage to b2cappaccessrightsethistoryrecord +-- +ALTER TABLE "B2CAppAccessRightSetHistoryRecord" ADD COLUMN "canExecuteSendVoIPStartMessage" boolean NULL; +-- +-- Recreate view for "B2CAppAccessRightSet" table +-- +DROP VIEW IF EXISTS "analytics"."B2CAppAccessRightSet"; +CREATE VIEW "analytics"."B2CAppAccessRightSet" AS (SELECT "app", "canExecuteSendVoIPStartMessage", "createdAt", "createdBy", "deletedAt", "dv", "id", "newId", "sender", "updatedAt", "updatedBy", "v" FROM "public"."B2CAppAccessRightSet"); + +COMMIT; + + `) +} + +exports.down = async (knex) => { + await knex.raw(` + BEGIN; +-- +-- Add field canExecuteSendVoIPStartMessage to b2cappaccessrightsethistoryrecord +-- +ALTER TABLE "B2CAppAccessRightSetHistoryRecord" DROP COLUMN "canExecuteSendVoIPStartMessage" CASCADE; +-- +-- Add field canExecuteSendVoIPStartMessage to b2cappaccessrightset +-- +ALTER TABLE "B2CAppAccessRightSet" DROP COLUMN "canExecuteSendVoIPStartMessage" CASCADE; +-- +-- Recreate view for "B2CAppAccessRightSet" table +-- +DROP VIEW IF EXISTS "analytics"."B2CAppAccessRightSet"; +CREATE VIEW "analytics"."B2CAppAccessRightSet" AS (SELECT "app", "createdAt", "createdBy", "deletedAt", "dv", "id", "newId", "sender", "updatedAt", "updatedBy", "v" FROM "public"."B2CAppAccessRightSet"); + +COMMIT; + + `) +} diff --git a/apps/condo/schema.graphql b/apps/condo/schema.graphql index 1bf3db16c3f..5638759c58e 100644 --- a/apps/condo/schema.graphql +++ b/apps/condo/schema.graphql @@ -83298,6 +83298,7 @@ type B2CAppAccessRightSetHistoryRecord { """ _label_: String app: String + canExecuteSendVoIPStartMessage: Boolean id: ID! v: Int createdAt: String @@ -83320,6 +83321,8 @@ input B2CAppAccessRightSetHistoryRecordWhereInput { app_not: String app_in: [String] app_not_in: [String] + canExecuteSendVoIPStartMessage: Boolean + canExecuteSendVoIPStartMessage_not: Boolean id: ID id_not: ID id_in: [ID] @@ -83403,6 +83406,8 @@ input B2CAppAccessRightSetHistoryRecordWhereUniqueInput { } enum SortB2CAppAccessRightSetHistoryRecordsBy { + canExecuteSendVoIPStartMessage_ASC + canExecuteSendVoIPStartMessage_DESC id_ASC id_DESC v_ASC @@ -83423,6 +83428,7 @@ enum SortB2CAppAccessRightSetHistoryRecordsBy { input B2CAppAccessRightSetHistoryRecordUpdateInput { app: String + canExecuteSendVoIPStartMessage: Boolean v: Int createdAt: String updatedAt: String @@ -83444,6 +83450,7 @@ input B2CAppAccessRightSetHistoryRecordsUpdateInput { input B2CAppAccessRightSetHistoryRecordCreateInput { app: String + canExecuteSendVoIPStartMessage: Boolean v: Int createdAt: String updatedAt: String @@ -83478,6 +83485,7 @@ type B2CAppAccessRightSet { """ Link to B2CApp """ app: B2CApp + canExecuteSendVoIPStartMessage: Boolean id: ID! v: Int createdAt: String @@ -83506,6 +83514,8 @@ input B2CAppAccessRightSetWhereInput { OR: [B2CAppAccessRightSetWhereInput] app: B2CAppWhereInput app_is_null: Boolean + canExecuteSendVoIPStartMessage: Boolean + canExecuteSendVoIPStartMessage_not: Boolean id: ID id_not: ID id_in: [ID] @@ -83571,6 +83581,8 @@ input B2CAppAccessRightSetWhereUniqueInput { enum SortB2CAppAccessRightSetsBy { app_ASC app_DESC + canExecuteSendVoIPStartMessage_ASC + canExecuteSendVoIPStartMessage_DESC id_ASC id_DESC v_ASC @@ -83591,6 +83603,7 @@ enum SortB2CAppAccessRightSetsBy { input B2CAppAccessRightSetUpdateInput { app: B2CAppRelateToOneInput + canExecuteSendVoIPStartMessage: Boolean v: Int createdAt: String updatedAt: String @@ -83609,6 +83622,7 @@ input B2CAppAccessRightSetsUpdateInput { input B2CAppAccessRightSetCreateInput { app: B2CAppRelateToOneInput + canExecuteSendVoIPStartMessage: Boolean v: Int createdAt: String updatedAt: String @@ -93722,6 +93736,103 @@ type SendB2BAppPushMessageOutput { id: String! } +input VoIPPanel { + """Dtfm command for panel""" + dtfmCommand: String! + + """Name of a panel to be displayed""" + name: String! +} + +input SendVoIPStartMessageDataForCallHandlingByB2CApp { + """Data that will be provided to B2CApp. May be stringified JSON""" + B2CAppContext: String! +} + +input SendVoIPStartMessageDataForCallHandlingByNative { + """Address of sip server, which device should connect to""" + voipAddress: String! + + """Login for connection to sip server""" + voipLogin: String! + + """Password for connection to sip server""" + voipPassword: String! + + """ + Panels and their commands to open. First one must be the main one. Multiple panels are in testing stage right now and may change + """ + voipPanels: [VoIPPanel!]! + + """ + Stun server urls. Are used to determine device public ip for media streams + """ + stunServers: [String!] + + """Preferred codec (usually vp8)""" + codec: String + + """ + Type of the native client to handle call. Values defined in mobile app. "0" used as legacy "sip" voipType for backwards compatibility. + """ + voipType: Int = 0 +} + +input SendVoIPStartMessageData { + """ + Unique value for each call session between panel and resident (means same for different devices also). + Must be provided for correct work with multiple devices that use same voip call. + F.e. to cancel calls with CANCELED_CALL_MESSAGE_PUSH messages + """ + callId: String! + + """ + If you want your B2CApp to handle incoming VoIP call, provide this argument. + """ + b2cAppCallData: SendVoIPStartMessageDataForCallHandlingByB2CApp + + """ + If you want mobile app to handle call (without your B2CApp), provide this argument. If "b2cAppCallData" and "nativeCallData" are provided together, native call is prioritized. + """ + nativeCallData: SendVoIPStartMessageDataForCallHandlingByNative +} + +input SendVoIPStartMessageInput { + dv: Int! + sender: SenderFieldInput! + app: B2CAppWhereUniqueInput! + + """ + Should be "addressKey" of B2CAppProperty / Property for which you want to send message + """ + addressKey: String! + + """Name of unit, same as in Property map""" + unitName: String! + + """Type of unit, same as in Property map""" + unitType: AllowedVoIPMessageUnitType! + callData: SendVoIPStartMessageData! +} + +type SendVoIPStartMessageOutput { + """ + Count of all Organization Contacts, which we possibly could've sent messages to + """ + verifiedContactsCount: Int + + """Count of Messages that will be sent, one for each verified Resident""" + createdMessagesCount: Int + + """Count of Messages which was not created due to some internal error""" + erroredMessagesCount: Int +} + +enum AllowedVoIPMessageUnitType { + flat + apartment +} + type unauthenticateUserOutput { """ `true` when unauthentication succeeds. @@ -106636,6 +106747,12 @@ type Mutation { "voipDtfmCommand": { "required": false }, + "voipPanels": { + "required": false + }, + "stunServers": { + "required": false + }, "stun": { "required": false }, @@ -108079,6 +108196,37 @@ type Mutation { """ sendB2BAppPushMessage(data: SendB2BAppPushMessageInput!): SendB2BAppPushMessageOutput + """ + Mutation sends VOIP_INCOMING_CALL Messages to each verified resident on address + unit. Also caches calls, so mobile app can properly react to cancel calls. You can either provide all data.* arguments so mobile app will use it's own app to answer call, or provide just B2CAppContext + callId to use your B2CApp's calling app + + + + **Errors** + + Following objects will be presented in `extensions` property of thrown error + + `{ + "code": "BAD_USER_INPUT", + "type": "PROPERTY_NOT_FOUND", + "message": "Unable to find Property or B2CAppProperty by provided addressKey", + "messageForUser": "Building not found for the provided \"addressKey\"" + }` + + `{ + "code": "BAD_USER_INPUT", + "type": "APP_NOT_FOUND", + "message": "Unable to find B2CApp.", + "messageForUser": "Application not found for the provided \"id\"" + }` + + `{ + "code": "BAD_USER_INPUT", + "type": "CALL_DATA_NOT_PROVIDED", + "message": "\"b2cAppCallData\" or \"nativeCallData\" or both should be provided" + }` + """ + sendVoIPStartMessage(data: SendVoIPStartMessageInput!): SendVoIPStartMessageOutput + """ Authenticate and generate a token for a User with the Password Authentication Strategy. """ authenticateUserWithPassword(email: String, password: String): authenticateUserOutput diff --git a/apps/condo/schema.ts b/apps/condo/schema.ts index 209ce7514b7..7ff7818dae0 100644 --- a/apps/condo/schema.ts +++ b/apps/condo/schema.ts @@ -1987,6 +1987,11 @@ export type AllMiniAppsWhereInput = { connected?: InputMaybe; }; +export enum AllowedVoIpMessageUnitType { + Apartment = 'apartment', + Flat = 'flat' +} + export type AmountDistributionField = { __typename?: 'AmountDistributionField'; amount: Scalars['String']['output']; @@ -8289,6 +8294,7 @@ export type B2CAppAccessRightSet = { _label_?: Maybe; /** Link to B2CApp */ app?: Maybe; + canExecuteSendVoIPStartMessage?: Maybe; createdAt?: Maybe; /** Identifies a user, which has created this record. It is a technical connection, that can represent real users, as well as automated systems (bots, scripts). This field should not participate in business logic. */ createdBy?: Maybe; @@ -8307,6 +8313,7 @@ export type B2CAppAccessRightSet = { export type B2CAppAccessRightSetCreateInput = { app?: InputMaybe; + canExecuteSendVoIPStartMessage?: InputMaybe; createdAt?: InputMaybe; createdBy?: InputMaybe; deletedAt?: InputMaybe; @@ -8330,6 +8337,7 @@ export type B2CAppAccessRightSetHistoryRecord = { */ _label_?: Maybe; app?: Maybe; + canExecuteSendVoIPStartMessage?: Maybe; createdAt?: Maybe; createdBy?: Maybe; deletedAt?: Maybe; @@ -8347,6 +8355,7 @@ export type B2CAppAccessRightSetHistoryRecord = { export type B2CAppAccessRightSetHistoryRecordCreateInput = { app?: InputMaybe; + canExecuteSendVoIPStartMessage?: InputMaybe; createdAt?: InputMaybe; createdBy?: InputMaybe; deletedAt?: InputMaybe; @@ -8369,6 +8378,7 @@ export enum B2CAppAccessRightSetHistoryRecordHistoryActionType { export type B2CAppAccessRightSetHistoryRecordUpdateInput = { app?: InputMaybe; + canExecuteSendVoIPStartMessage?: InputMaybe; createdAt?: InputMaybe; createdBy?: InputMaybe; deletedAt?: InputMaybe; @@ -8390,6 +8400,8 @@ export type B2CAppAccessRightSetHistoryRecordWhereInput = { app_in?: InputMaybe>>; app_not?: InputMaybe; app_not_in?: InputMaybe>>; + canExecuteSendVoIPStartMessage?: InputMaybe; + canExecuteSendVoIPStartMessage_not?: InputMaybe; createdAt?: InputMaybe; createdAt_gt?: InputMaybe; createdAt_gte?: InputMaybe; @@ -8490,6 +8502,7 @@ export type B2CAppAccessRightSetRelateToOneInput = { export type B2CAppAccessRightSetUpdateInput = { app?: InputMaybe; + canExecuteSendVoIPStartMessage?: InputMaybe; createdAt?: InputMaybe; createdBy?: InputMaybe; deletedAt?: InputMaybe; @@ -8506,6 +8519,8 @@ export type B2CAppAccessRightSetWhereInput = { OR?: InputMaybe>>; app?: InputMaybe; app_is_null?: InputMaybe; + canExecuteSendVoIPStartMessage?: InputMaybe; + canExecuteSendVoIPStartMessage_not?: InputMaybe; createdAt?: InputMaybe; createdAt_gt?: InputMaybe; createdAt_gte?: InputMaybe; @@ -49948,6 +49963,12 @@ export type Mutation = { * "voipDtfmCommand": { * "required": false * }, + * "voipPanels": { + * "required": false + * }, + * "stunServers": { + * "required": false + * }, * "stun": { * "required": false * }, @@ -50871,6 +50892,36 @@ export type Mutation = { * }` */ sendOrganizationEmployeeRequest?: Maybe; + /** + * Mutation sends VOIP_INCOMING_CALL Messages to each verified resident on address + unit. Also caches calls, so mobile app can properly react to cancel calls. You can either provide all data.* arguments so mobile app will use it's own app to answer call, or provide just B2CAppContext + callId to use your B2CApp's calling app + * + * + * + * **Errors** + * + * Following objects will be presented in `extensions` property of thrown error + * + * `{ + * "code": "BAD_USER_INPUT", + * "type": "PROPERTY_NOT_FOUND", + * "message": "Unable to find Property or B2CAppProperty by provided addressKey", + * "messageForUser": "Building not found for the provided \"addressKey\"" + * }` + * + * `{ + * "code": "BAD_USER_INPUT", + * "type": "APP_NOT_FOUND", + * "message": "Unable to find B2CApp.", + * "messageForUser": "Application not found for the provided \"id\"" + * }` + * + * `{ + * "code": "BAD_USER_INPUT", + * "type": "CALL_DATA_NOT_PROVIDED", + * "message": "\"b2cAppCallData\" or \"nativeCallData\" or both should be provided" + * }` + */ + sendVoIPStartMessage?: Maybe; setMessageStatus?: Maybe; setPaymentPosReceiptUrl?: Maybe; shareTicket?: Maybe; @@ -58717,6 +58768,11 @@ export type MutationSendOrganizationEmployeeRequestArgs = { }; +export type MutationSendVoIpStartMessageArgs = { + data: SendVoIpStartMessageInput; +}; + + export type MutationSetMessageStatusArgs = { data: SetMessageStatusInput; }; @@ -89799,6 +89855,64 @@ export type SendOrganizationEmployeeRequestInput = { sender: SenderFieldInput; }; +export type SendVoIpStartMessageData = { + /** If you want your B2CApp to handle incoming VoIP call, provide this argument. */ + b2cAppCallData?: InputMaybe; + /** + * Unique value for each call session between panel and resident (means same for different devices also). + * Must be provided for correct work with multiple devices that use same voip call. + * F.e. to cancel calls with CANCELED_CALL_MESSAGE_PUSH messages + */ + callId: Scalars['String']['input']; + /** If you want mobile app to handle call (without your B2CApp), provide this argument. If "b2cAppCallData" and "nativeCallData" are provided together, native call is prioritized. */ + nativeCallData?: InputMaybe; +}; + +export type SendVoIpStartMessageDataForCallHandlingByB2CApp = { + /** Data that will be provided to B2CApp. May be stringified JSON */ + B2CAppContext: Scalars['String']['input']; +}; + +export type SendVoIpStartMessageDataForCallHandlingByNative = { + /** Preferred codec (usually vp8) */ + codec?: InputMaybe; + /** Stun server urls. Are used to determine device public ip for media streams */ + stunServers?: InputMaybe>; + /** Address of sip server, which device should connect to */ + voipAddress: Scalars['String']['input']; + /** Login for connection to sip server */ + voipLogin: Scalars['String']['input']; + /** Panels and their commands to open. First one must be the main one. Multiple panels are in testing stage right now and may change */ + voipPanels: Array; + /** Password for connection to sip server */ + voipPassword: Scalars['String']['input']; + /** Type of the native client to handle call. Values defined in mobile app. "0" used as legacy "sip" voipType for backwards compatibility. */ + voipType?: InputMaybe; +}; + +export type SendVoIpStartMessageInput = { + /** Should be "addressKey" of B2CAppProperty / Property for which you want to send message */ + addressKey: Scalars['String']['input']; + app: B2CAppWhereUniqueInput; + callData: SendVoIpStartMessageData; + dv: Scalars['Int']['input']; + sender: SenderFieldInput; + /** Name of unit, same as in Property map */ + unitName: Scalars['String']['input']; + /** Type of unit, same as in Property map */ + unitType: AllowedVoIpMessageUnitType; +}; + +export type SendVoIpStartMessageOutput = { + __typename?: 'SendVoIPStartMessageOutput'; + /** Count of Messages that will be sent, one for each verified Resident */ + createdMessagesCount?: Maybe; + /** Count of Messages which was not created due to some internal error */ + erroredMessagesCount?: Maybe; + /** Count of all Organization Contacts, which we possibly could've sent messages to */ + verifiedContactsCount?: Maybe; +}; + export type SenderField = { __typename?: 'SenderField'; dv: Scalars['Int']['output']; @@ -91396,6 +91510,8 @@ export enum SortB2CAppAccessRightHistoryRecordsBy { } export enum SortB2CAppAccessRightSetHistoryRecordsBy { + CanExecuteSendVoIpStartMessageAsc = 'canExecuteSendVoIPStartMessage_ASC', + CanExecuteSendVoIpStartMessageDesc = 'canExecuteSendVoIPStartMessage_DESC', CreatedAtAsc = 'createdAt_ASC', CreatedAtDesc = 'createdAt_DESC', DeletedAtAsc = 'deletedAt_ASC', @@ -91417,6 +91533,8 @@ export enum SortB2CAppAccessRightSetHistoryRecordsBy { export enum SortB2CAppAccessRightSetsBy { AppAsc = 'app_ASC', AppDesc = 'app_DESC', + CanExecuteSendVoIpStartMessageAsc = 'canExecuteSendVoIPStartMessage_ASC', + CanExecuteSendVoIpStartMessageDesc = 'canExecuteSendVoIPStartMessage_DESC', CreatedAtAsc = 'createdAt_ASC', CreatedAtDesc = 'createdAt_DESC', CreatedByAsc = 'createdBy_ASC', @@ -117562,6 +117680,13 @@ export enum VillageMapType { Village = 'village' } +export type VoIpPanel = { + /** Dtfm command for panel */ + dtfmCommand: Scalars['String']['input']; + /** Name of a panel to be displayed */ + name: Scalars['String']['input']; +}; + /** Webhooks are a way that the APP can send automated web callback with some messages to other apps or system to inform them about any updates. How does it work: 1. When objects are created or changed, we make requests to the GraphQL API to get data on behalf of the specified user; 2. Then we send the data to remote url. Webhook model contains basic configuration of integration, such as external server url, name, encryption parameters and so on. */ export type Webhook = { __typename?: 'Webhook';