@iotize/ionic
Version:
Iotize specific building blocks on top of @ionic/angular.
803 lines (801 loc) • 99.1 kB
JavaScript
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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