UNPKG

@iotize/ionic

Version:

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

803 lines (801 loc) 99.1 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 { 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; /** * Hack to prevent angular treeshaking from removing loaded extension. */ this._loadedTapExtensions = [ _TAP_EXTENSION_DATA_, _TAP_EXTENSION_DATA_LOG_, _TAP_EXTENSION_KEEP_ALIVE_, _TAP_SERVICE_ALL_EXTENSIONS_, _TAP_EXTENSION_AUTH_, factoryReset, ]; this._tapOrUndefined = new BehaviorSubject(undefined); /** * Only connection lost event */ this._connectionLost$ = new Subject(); this._sessionState$ = this._tapOrUndefined.pipe(switchMap((tapDeviceOrUndefined) => { if (!tapDeviceOrUndefined) { return of(INITIAL_SESSION_STATE); } else { return tapDeviceOrUndefined.auth.sessionState; } })); this.listeners = []; this.keepAlivePeriod = 10 * 1000; this.meta = {}; /** * @deprecated */ this._tapConfig$ = new BehaviorSubject(undefined); /** * Event trigger when currrent Tap changed (set or unset) * If tap is removed, value will be undefined * Immediatly triggered the current value when subscribing */ this.tapOrUndefinedChangedWithLastValue = this._tapOrUndefined.pipe(distinctUntilChanged()); /** * Event trigger when currrent Tap changed (set or unset) * If tap is removed, value will be undefined */ this.tapOrUndefinedChanged = this.tapOrUndefinedChangedWithLastValue.pipe(skip(1)); /** * Event trigger when a new Tap is selected (no event when tap is removed) */ this.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 */ this.tapChangedWithLastValue = this.tapOrUndefinedChangedWithLastValue.pipe(filter((tap) => !!tap)); /** * Event triggered when tap is removed */ this.tapRemoved = new Subject(); this._protocolMeta = new BehaviorSubject(undefined); this._availableProtocols = new BehaviorSubject([]); this._isManualDisconnection = false; this._sessionStateSnapshot = INITIAL_SESSION_STATE; /** * Connection state events with replay. * It works even when tap is changed */ this.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 */ this._connectionState$ = this.connectionStateReplay.pipe(skip(1)); 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; } /** * 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$; } /** * @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._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 */ CurrentDeviceService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CurrentDeviceService, deps: [{ token: i1.Platform }, { token: i1.ToastController }, { token: i2.ProtocolFactoryService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ CurrentDeviceService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CurrentDeviceService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CurrentDeviceService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: i1.Platform }, { type: i1.ToastController }, { type: i2.ProtocolFactoryService }, { type: i0.NgZone }]; } }); //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3VycmVudC1kZXZpY2Uuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lvdGl6ZS1pb25pYy9zcmMvbGliL2N1cnJlbnQtZGV2aWNlLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFFbkQsT0FBTyxFQUFFLFFBQVEsRUFBRSxlQUFlLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUMzRCxPQUFPLEVBQWEsV0FBVyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDOUQsT0FBTyxFQUFXLG1CQUFtQixFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFDOUUsT0FBTyxFQUNMLFlBQVksRUFDWixHQUFHLEVBQ0gsUUFBUSxFQUNSLFdBQVcsRUFDWCxzQkFBc0IsR0FDdkIsTUFBTSxhQUFhLENBQUM7QUFDckIsT0FBTyxFQUNMLHFCQUFxQixFQUVyQixvQkFBb0IsR0FDckIsTUFBTSxrQkFBa0IsQ0FBQztBQUMxQixPQUFPLEVBR0wsVUFBVSxHQUVYLE1BQU0sd0JBQXdCLENBQUM7QUFDaEMsT0FBTyxFQUFhLGdCQUFnQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFFdEUsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDNUQsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDcEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzdELE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBRXhFLE9BQU8sRUFDTCxXQUFXLEVBQ1gsZUFBZSxHQUVoQixNQUFNLDBCQUEwQixDQUFDO0FBQ2xDLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZFLE9BQU8sRUFDTCxlQUFlLEVBR2YsT0FB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