UNPKG

@iotize/ionic

Version:

Iotize specific building blocks on top of @ionic/angular.

830 lines (828 loc) 102 kB
import { Injectable, NgZone } from '@angular/core'; import { Platform, ToastController } from '@ionic/angular'; import { isCodeError } from '@iotize/common/error'; import { parseTapNdefMessage } from '@iotize/device-com-nfc.cordova'; import { HostProtocol, Tap, TapError, TapResponse, TapResponseStatusError, } from '@iotize/tap'; import { INITIAL_SESSION_STATE, _TAP_EXTENSION_AUTH_, } from '@iotize/tap/auth'; import { ResultCode, } from '@iotize/tap/client/api'; import { TapRequestHelper } from '@iotize/tap/client/impl'; import { _TAP_EXTENSION_DATA_ } from '@iotize/tap/ext/data'; import { _TAP_EXTENSION_DATA_LOG_ } from '@iotize/tap/ext/data-log'; import { factoryReset } from '@iotize/tap/ext/factory-reset'; import { _TAP_EXTENSION_KEEP_ALIVE_ } from '@iotize/tap/ext/keep-alive'; import { ComProtocol, ConnectionState, } from '@iotize/tap/protocol/api'; import { _TAP_SERVICE_ALL_EXTENSIONS_, } from '@iotize/tap/service/all'; import { BehaviorSubject, Subject, of, } from 'rxjs'; import { distinctUntilChanged, filter, shareReplay, skip, startWith, switchMap, tap, } from 'rxjs/operators'; import './extensions'; import { DataManagerIonic } from './extensions/data-manager'; import { debug } from './logger'; import { isSameTag } from './nfc/utility'; import { ProtocolFactoryService } from './protocol-factory.service'; import { runInZone } from './rx-utility/run-in-zone'; import { createBleName } from './utility'; import * as i0 from "@angular/core"; import * as i1 from "@ionic/angular"; import * as i2 from "./protocol-factory.service"; const TAG = 'CurrentDeviceService'; export function LONG_RANGE_PROTOCOL_FILTER(meta) { return meta.type !== 'nfc'; } function getProtocolOrUndefined(client) { try { return client.getCurrentProtocol(); } catch (err) { return undefined; } } export class TapServiceError extends Error { static illegalArgument(msg) { throw new Error(`Illegal argument: ${msg}`); } static illegalStateNoTap() { return new TapServiceError('Illegal state: Tap is not set yet'); } } export class CurrentDeviceService { platform; toastCtrl; protocolFactory; ngZone; /** * Hack to prevent angular treeshaking from removing loaded extension. */ _loadedTapExtensions = [ _TAP_EXTENSION_DATA_, _TAP_EXTENSION_DATA_LOG_, _TAP_EXTENSION_KEEP_ALIVE_, _TAP_SERVICE_ALL_EXTENSIONS_, _TAP_EXTENSION_AUTH_, factoryReset, ]; _tapOrUndefined = new BehaviorSubject(undefined); _tap; /** * Only connection lost event */ _connectionLost$ = new Subject(); _sessionState$ = this._tapOrUndefined.pipe(switchMap((tapDeviceOrUndefined) => { if (!tapDeviceOrUndefined) { return of(INITIAL_SESSION_STATE); } else { return tapDeviceOrUndefined.auth.sessionState; } })); listeners = []; keepAlivePeriod = 10 * 1000; meta = {}; /** * @deprecated */ _dataManager; /** * @deprecated */ _tapConfig$ = new BehaviorSubject(undefined); _maxFrameSizeCache = {}; /** * Event trigger when currrent Tap changed (set or unset) * If tap is removed, value will be undefined * Immediatly triggered the current value when subscribing */ tapOrUndefinedChangedWithLastValue = this._tapOrUndefined.pipe(distinctUntilChanged()); /** * Event trigger when currrent Tap changed (set or unset) * If tap is removed, value will be undefined */ tapOrUndefinedChanged = this.tapOrUndefinedChangedWithLastValue.pipe(skip(1)); /** * Event trigger when a new Tap is selected (no event when tap is removed) */ tapChanged = this.tapOrUndefinedChanged.pipe(filter((tap) => !!tap)); /** * Event trigger when a new Tap is selected (no event when tap is removed) * Immediatly triggered the current value when subscribing */ tapChangedWithLastValue = this.tapOrUndefinedChangedWithLastValue.pipe(filter((tap) => !!tap)); /** * Event triggered when tap is removed */ tapRemoved = new Subject(); _protocolMeta = new BehaviorSubject(undefined); _availableProtocols = new BehaviorSubject([]); _isManualDisconnection = false; _sessionStateSnapshot = INITIAL_SESSION_STATE; get sessionStateSnapshot() { return this._sessionStateSnapshot; } get sessionState() { return this._sessionState$; } get protocolMeta$() { return this._protocolMeta.asObservable(); } get availableProtocols$() { return this._availableProtocols.asObservable(); } get protocolMeta() { return this._protocolMeta.value; } set protocolMeta(meta) { if (meta) { this.addProtocolMeta(meta); } debug(TAG, 'setting protocol meta', meta); this._protocolMeta.next(meta); } get connectionLost() { return this._connectionLost$.asObservable(); } get availableProtocols() { return this._availableProtocols.value; } get tap() { if (!this._tap) { throw new Error('Connect to a device first'); // throw TapServiceError.illegalStateNoTap(); } return this._tap; } get tapOrUndefined() { return this._tap; } get hasTap() { return this._tap !== undefined; } set tapConfig(schema) { debug(TAG, 'Set Tap config', schema); this.dataManager = new DataManagerIonic(this.tap); this._tapConfig$.next(schema); if (schema) { if (schema.config?.data) { // this.tap.data.clear(); this.tap.data.configureWithDataConfig(schema.config.data); /* this.tap.data.values.subscribe(v => { }) this.tap.data.monitoring.asSubject() .subscribe(state => { }) */ } else { this.tap.data.clear(); } // // this.dataLogger.converter = new DataLogPacketConverter( // newBundleConfigToOldBundleConfig(schema.config.data.bundles) // ); // debug(TAG, 'Updating datalog converter'); // this.dataLogger.converter = DataLogPacketConverter.createFromManager(this.tap.bundles, this.tap.variables); // if (schema.config.data) { // this.dataLogger.converter = new DataLogPacketConverter( // newBundleConfigToOldBundleConfig(schema.config.data.bundles || []) // ); // } } } get tapConfig() { return this._tapConfig$.value; } get tapConfig$() { return this._tapConfig$; } isSameTag(tag) { if (this.protocolMeta && this.protocolMeta.type === 'nfc') { // Check is it's the same tag const currentTag = this.protocolMeta.info.tag; if (currentTag.id && currentTag.id?.length == 0) { return false; } return isSameTag(currentTag, tag); } else { // It's not nfc currently const nfcProtocolMeta = this.availableProtocols.find((p) => p.type === 'nfc'); if (nfcProtocolMeta) { return isSameTag(nfcProtocolMeta.info.tag, tag); } } return false; } /** * Use another communicaiton protocol * May be rejected * @param meta: ProtocolMeta * @param disonnectCurrentProtocol if set to true and if tap is already connected with * a communication protocol, it will disconnect from it first * @param connectToNew: boolean */ async useProtocol(meta, disonnectCurrentProtocol = true, connectToNew = true) { debug(TAG, 'use protocol', meta); const protocol = await this.protocolFactory.create(meta); if (!this._tap) { this.tap = Tap.fromProtocol(protocol); } else { // let oldConnectionState = this._tap.protocol.getConnectionState(); if (disonnectCurrentProtocol) { try { await this.tap.protocol.disconnect().toPromise(); } catch (err) { console.warn('Cannot disconnect current protocol properly: ', err); } } this._tap.useComProtocol(protocol); } this.protocolMeta = meta; if (connectToNew) { await this.connect(); } } async executeFactoryReset() { await this.tap.factoryReset(); this.tap.auth.clearCache(); this.tap.encryption.stop(); } set tap(t) { this.setTap(t, { emit: true }); } constructor(platform, toastCtrl, protocolFactory, ngZone) { this.platform = platform; this.toastCtrl = toastCtrl; this.protocolFactory = protocolFactory; this.ngZone = ngZone; debug(TAG, 'NEW INSTANCE', this._loadedTapExtensions); this.tapChangedWithLastValue.subscribe((newTap) => { newTap.client.addInterceptor((context, next) => { return next.handle(context).pipe(tap((tapResponseFrame) => { const response = new TapResponse(tapResponseFrame, context.request); if (response.status === ResultCode.UNAUTHORIZED) { if (TapRequestHelper.pathToString(context.request.header.path) !== this.tap.service.interface.resources.login.path) { this.listeners.forEach((listener) => { if (listener.onTapRequestUnauthorized) { listener.onTapRequestUnauthorized({ request: context, response: response, }); } }); } } if (!response.isSuccessful()) { this.listeners.forEach((listener) => { if (listener.onTapRequestError) { listener.onTapRequestError({ request: context.request, error: new TapResponseStatusError(response), response: response, }); } }); } }, (error) => { this.listeners.forEach((listener) => { console.warn(`Request ${context.request} errored: ${error}`); if (listener.onTapRequestError) { listener.onTapRequestError({ request: context.request, error: error, }); } }); })); }); }); this.sessionState.subscribe((newSessionState) => { this._sessionStateSnapshot = newSessionState; }); this.connectionStateReplay.subscribe(async (event) => { if (event.newState === ConnectionState.DISCONNECTED) { this.keepAliveEngine?.stop(); if (!this._isManualDisconnection) { this._connectionLost$?.next(event); } } else if (event.newState == ConnectionState.CONNECTED) { this.ngZone.runOutsideAngular(() => { if (this.keepAlivePeriod > 0) { debug(TAG, 'Start keep alive with period', this.keepAlivePeriod); this.keepAliveEngine?.start(); } else { debug(TAG, 'NO keep alive'); } }); } this.listeners.forEach((listener) => listener.onTapConnectionStateChange(event)); }); } addProtocolMeta(meta) { if (meta) { const protocols = this.availableProtocols; const protocolIndex = protocols.findIndex((existing) => { return existing.type === meta.type; }); if (protocolIndex >= 0) { debug(TAG, 'Protocol meta', meta, 'already exists. Replacing infos'); protocols[protocolIndex] = meta; this._availableProtocols.next(protocols); } else { debug(TAG, 'Adding protocol meta', meta); protocols.push(meta); this._availableProtocols.next(protocols); } } } /** * Parse NDefTag and try to create a ProtocolMeta thanks to record * @returns undefined if there is no other protocol in the tag */ async registerProtocolsFromTag(tag) { let meta; if (!tag.ndefMessage) { return undefined; } const info = parseTapNdefMessage(tag.ndefMessage); if (info.macAddress && info.macAddress !== '00:00:00:00:00:00') { meta = { type: 'ble', info: { mac: info.macAddress, name: info.name, }, }; } else if (info.ssid) { const hostname = (await this.tap.service.wifi.getHostname()).body(); meta = { type: 'wifi', info: { ssid: info.ssid, // name: info.name, url: `tcp://${hostname}:2000`, }, }; } if (meta) { this.addProtocolMeta(meta); } return meta; } async getCurrentHostProtocolMaxFrameSizeCacheFirst() { const protocol = this.protocolMeta?.type; if (!protocol || !this._maxFrameSizeCache[protocol]) { const hostProtocol = (await this.tap.service.interface.getCurrentHostProtocolMaxFrameSize()).body(); if (hostProtocol.request > 0xff) { hostProtocol.request -= 2; // due to 2 more bytes for apdu.header.lc } if (protocol) { this._maxFrameSizeCache[protocol] = hostProtocol; } else { return hostProtocol; } } return this._maxFrameSizeCache[protocol]; } /** * Will register available communication protocols on current tap * by asking LWM2M resources. * @returns the list of new ProtocolMeta found */ async registerProtocolsFromTap() { const nProtocolMeta = []; const [appNameResponse, serialNumberResponse, authorizedHostProtocolsResponse, bleAddressResponse, ipResponse,] = await this.tap.service.interface.executeMultipleCalls([ this.tap.service.interface.getAppNameCall(), this.tap.service.device.getSerialNumberCall(), this.tap.service.interface.getAuthorizedHostProtocolCall(), this.tap.service.ble.getAddressCall(), this.tap.service.wifi.getIpCall(), ]); const authorizedHostProtocols = authorizedHostProtocolsResponse.body(); if (this.protocolMeta?.type !== 'ble' && authorizedHostProtocols.includes(HostProtocol.BLE)) { if (this.platform.is('ios')) { const bleName = createBleName(appNameResponse.body(), serialNumberResponse.body()); nProtocolMeta.push({ type: 'ble', info: { name: bleName, }, }); } else { nProtocolMeta.push({ type: 'ble', info: { mac: bleAddressResponse.body(), }, }); } } if (this.protocolMeta?.type !== 'socket' && this.platform.is('mobile') && authorizedHostProtocols.includes(HostProtocol.WIFI)) { const ip = ipResponse.body(); if (ip && ip !== '0.0.0.0') { nProtocolMeta.push({ type: 'socket', info: { url: 'tcp://' + ip + ':2000', }, }); } } nProtocolMeta.forEach((meta) => this.addProtocolMeta(meta)); return nProtocolMeta; } async useProtocolFromMeta(newProtocolMeta, disonnectCurrentProtocol = true, connectToNew = true) { if (!this.protocolMeta || newProtocolMeta.type !== this.protocolMeta.type) { return await this.useProtocol(newProtocolMeta, disonnectCurrentProtocol, connectToNew); } } /** * Switch from NFC communication protocol to a long range communication * @warning this is only available when we are in NFC * * @return the new protocol meta used or undefined if it does not have a long range protocol to use */ async useLongRangeProtocol() { debug(TAG, 'useLongRangeProtocol'); const currentProtocol = this.protocolMeta; if (!this._tap) { throw TapServiceError.illegalStateNoTap(); } if (currentProtocol) { if (currentProtocol.type === 'nfc') { const protocolMeta = this.availableProtocols.find(LONG_RANGE_PROTOCOL_FILTER); if (protocolMeta) { debug(TAG, 'Using long range protocol: ', protocolMeta); await this.useProtocolFromMeta(protocolMeta); return protocolMeta; } else { debug(TAG, 'NFC tag does not have long range protocol information...'); } } } return undefined; } /** * Connection state events. * It works even when tap is changed */ get connectionState() { return this._connectionState$; } /** * Connection state events with replay. * It works even when tap is changed */ connectionStateReplay = this.tapOrUndefinedChangedWithLastValue.pipe(switchMap((tapDevice) => { if (!tapDevice) { return of({ newState: ConnectionState.DISCONNECTED, oldState: ConnectionState.DISCONNECTED, }); } const currentProtocol = getProtocolOrUndefined(tapDevice.client); return tapDevice.client.onProtocolChange().pipe(startWith({ newProtocol: currentProtocol, }), switchMap((event) => { if (!event.newProtocol) { return of({ newState: ConnectionState.DISCONNECTED, oldState: ConnectionState.DISCONNECTED, }); } return event.newProtocol.onConnectionStateChange().pipe(runInZone(this.ngZone), startWith({ newState: event.newProtocol.getConnectionState(), oldState: ConnectionState.DISCONNECTED, })); })); }), shareReplay(1)); /** * Any connection state change */ _connectionState$ = this.connectionStateReplay.pipe(skip(1)); /** * @returns true if user is connected as given username or one of the given usernames */ async isLoggedInAsUserOrProfileName(userOrProfileNames) { if (typeof userOrProfileNames === 'string') { userOrProfileNames = [userOrProfileNames]; } if (typeof userOrProfileNames === 'number') { console.warn('invalid parameter for CurrentDeviceService.isLoggedInAsUserOrProfileName() username should be a string'); userOrProfileNames = [userOrProfileNames.toString()]; } if (!this._tap || !this.tap.auth.sessionStateSnapshot) { return false; } const sessionState = this.tap.auth.sessionStateSnapshot || (await this.tap.auth.refreshSessionState()); return (userOrProfileNames.includes(sessionState.profileName) || userOrProfileNames.includes(sessionState.name)); } async login(username, password, refreshSessionState = true) { return await this.tap.auth.login({ username, password }, { noRefreshSessionState: !refreshSessionState, }); } async logout(throwErr = false) { if (!this._tap) { return false; } try { await this._tap.auth.logout(); return true; } catch (err) { if (throwErr) { throw err; } else { this.onError.bind(this); return false; } } } setTap(newTap, options = { emit: true }) { if (!newTap) { throw new Error('Illegal state: cannot set undefined tap'); } this._tap = newTap; debug(TAG, 'Setting tap...'); this.meta = {}; this._tap = newTap; if (this.keepAlivePeriod === 0) { newTap.keepAlive.stop(); } newTap.keepAlive.period = this.keepAlivePeriod; if (options.emit) { this.notifyNewTap(); } } notifyNewTap() { this._tapOrUndefined.next(this._tap); } setTapFromEvent(event, options = { emit: true }) { if (!event.protocolMeta) { throw new Error('Missing protocol information'); } this.protocolMeta = event.protocolMeta; // this.tap = event.tap; this.setTap(event.tap, options); debug(TAG, 'setTapFromEvent = >', event); } /** * Connect to the Tap and refresh session state * Throw error if it fails */ async connect( /** * @deprecated */ throwErr = true) { try { await this.tap.connect(); await this.refreshSessionState(); return true; } catch (err) { debug(TAG, 'Throwing error on connection failed'); throw err; } } /** * Disconnect Tap */ async disconnect() { debug(TAG, 'disconnect'); if (!this.hasProtocol()) { return; } this._isManualDisconnection = true; debug(TAG, 'Setting _isManualDisconnection flag to true'); return (this._tap ? this._tap.disconnect() : Promise.reject(TapServiceError.illegalStateNoTap())).then((res) => { // This is a temporary hack to differentiate manual/unexpected disconnection // The timeout is used to make sure that direct events are trigger before we set the manual // disconnection flag back to false // TODO remove later setTimeout(() => { debug(TAG, 'Setting _isManualDisconnection flag to false'); this._isManualDisconnection = false; }, 50); return res; }); } /** * Remove currently used Tap * If no Tap was set, do nothing */ async remove(disconnect = true) { if (this._tap) { const oldTap = this._tap; debug(TAG, 'Removing tap'); this.protocolMeta = undefined; this._availableProtocols.next([]); if (disconnect) { await this.disconnect().catch(this.onError.bind(this)); } console.log('Stopping old keep alive', oldTap.keepAlive); oldTap.keepAlive.stop(); this._tap = undefined; this._dataManager = undefined; this._maxFrameSizeCache = {}; // this._tapConfig = undefined; this.notifyTapRemoved(oldTap); } else { debug(TAG, 'No tap to remove'); } } notifyTapRemoved(t) { this.tapRemoved.next(t); this.notifyNewTap(); } /** * @deprecated */ async configureClientIfRequired() { if (!this.meta.isClientConfigured) { console.log('Configuring client...'); await this.configureClient().catch((err) => { this.onError(err); }); } } /** * Rebuild tap configuration according to data on the Tap * * @deprecated */ async loadConfigFromDevice() { debug(TAG, 'dynamically load Tap config'); const syncEvent = await this.tap.data.synchronizeTapConfig().toPromise(); this.meta.isClientConfigured = true; // TODO Fix to get real config if (syncEvent?.step === 'done') { const bundles = syncEvent.bundles; this.tapConfig = { meta: { version: '1.0.0', partial: true, }, config: { version: 1, data: { bundles: [], }, }, }; } } set dataManager(v) { debug(TAG, 'Setting data manager', v); if (this._dataManager) { this._dataManager.destroy(); } this._dataManager = v; } get dataManager() { if (!this._dataManager) { return undefined; } return this._dataManager; } /** * Configure client by reading tap device configuration * * @deprecated will be moved to a separate service * * @throws */ async configureClient(refresh = false) { debug(TAG, 'configureClient'); if (refresh) { this.tapConfig = undefined; } // TODO only once // if (this.modelConfig) { // console.info('load from iotz model configuration file') // let tapConfigConfigurator = new TapConfigConfigurator(this.modelConfig); // await this.tap.configure(tapConfigConfigurator); // this.meta.isClientConfigured = true; // console.log('Variables: ', this.tap.variables); // } if (this.tapConfig === undefined) { await this.loadConfigFromDevice(); // this.tap.bundles.clear(); // this.tap.variables.clear(); // await this.tap.configure(this.readDeviceDataConfigurator).catch(this.onError.bind(this)); } } getCurrentProtocol() { return getProtocolOrUndefined(this.tap.client); } get keepAliveEngine() { return this._tap?.keepAlive; } hasProtocol() { return this.getCurrentProtocol() !== undefined; } registerEventListerner(listener) { this.listeners.push(listener); } unregisterEventListener(listener) { const index = this.listeners.indexOf(listener); if (index >= 0) { this.listeners.splice(index, 1); } } /** * @deprecated refractor: remove on error from this service to use a global handler * * @param err */ async onError(err) { // TODO remove const toast = await this.toastCtrl.create({ message: err.message || 'Unknown error', color: 'danger', position: 'bottom', duration: 3000, buttons: [ { text: 'Close', role: 'cancel', handler: () => { console.log('Close clicked'); }, }, ], }); await toast.present(); } async reboot() { let rebootResponse; try { rebootResponse = await this.tap.service.device.reboot(); } catch (err) { if (!isCodeError(TapError.Code.ExecuteRequestError, err)) { throw err; } else { const cause = err.cause; if (cause?.code !== ComProtocol.ErrorCode.TimeoutError) { console.warn('Reboot error', err); throw err; } // ignore timeout error as tap may reboot before sending response } } if (rebootResponse) { rebootResponse.successful(); } await this.clearAuth(); this.tap.disconnect().catch((err) => { }); } async clearAuth() { this.tap.auth.clearCache(); this.tap.encryption.stop(); await this.tap.auth.logout().catch((err) => { }); } async refreshSessionState() { try { return await this.tap.auth.refreshSessionState(); } catch (err) { if (isCodeError(TapError.Code.ScramNotStartedYet, err) || isCodeError(TapError.Code.InvalidScramKey, err)) { console.warn(`SCRAM session ended due to error`, err.message); this.tap.encryption.stop(); return await this.tap.auth.refreshSessionState(); } else { throw err; } } } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CurrentDeviceService, deps: [{ token: i1.Platform }, { token: i1.ToastController }, { token: i2.ProtocolFactoryService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CurrentDeviceService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CurrentDeviceService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: i1.Platform }, { type: i1.ToastController }, { type: i2.ProtocolFactoryService }, { type: i0.NgZone }] }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3VycmVudC1kZXZpY2Uuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lvdGl6ZS1pb25pYy9zcmMvbGliL2N1cnJlbnQtZGV2aWNlLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFFbkQsT0FBTyxFQUFFLFFBQVEsRUFBRSxlQUFlLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMzRCxPQUFPLEVBQWEsV0FBVyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDOUQsT0FBTyxFQUFXLG1CQUFtQixFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDOUUsT0FBTyxFQUNMLFlBQVksRUFDWixHQUFHLEVBQ0gsUUFBUSxFQUNSLFdBQVcsRUFDWCxzQkFBc0IsR0FDdkIsTUFBTSxhQUFhLENBQUM7QUFDckIsT0FBTyxFQUNMLHFCQUFxQixFQUVyQixvQkFBb0IsR0FDckIsTUFBTSxrQkFBa0IsQ0FBQztBQUMxQixPQUFPLEVBR0wsVUFBVSxHQUVYLE1BQU0sd0JBQXdCLENBQUM7QUFDaEMsT0FBTyxFQUFhLGdCQUFnQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFFdEUsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDNUQsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDcEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzdELE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBRXhFLE9BQU8sRUFDTCxXQUFXLEVBQ1gsZUFBZSxHQUVoQixNQUFNLDBCQUEwQixDQUFDO0FBQ2xDLE9BQU8sRUFFTCw0QkFBNEIsR0FDN0IsTUFBTSx5QkFBeUIsQ0FBQztBQUNqQyxPQUFPLEVBQ0wsZUFBZSxFQUdmLE9B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