2017-12-20 23:28:43 -05:00
|
|
|
import { Component } from "@angular/core";
|
2017-12-22 23:08:10 -05:00
|
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
2017-12-24 04:02:57 -05:00
|
|
|
import { ScalarClientApiService } from "../../shared/services/scalar/scalar-client-api.service";
|
2017-12-15 01:41:56 -05:00
|
|
|
import * as _ from "lodash";
|
2017-12-24 04:02:57 -05:00
|
|
|
import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service";
|
2021-08-16 18:00:59 -04:00
|
|
|
import {
|
2021-09-01 19:01:01 -04:00
|
|
|
FE_Integration,
|
|
|
|
FE_IntegrationRequirement,
|
|
|
|
FE_SimpleBot,
|
2021-08-16 18:00:59 -04:00
|
|
|
} from "../../shared/models/integration";
|
2017-12-24 04:02:57 -05:00
|
|
|
import { IntegrationsRegistry } from "../../shared/registry/integrations.registry";
|
2017-12-22 23:08:10 -05:00
|
|
|
import { SessionStorage } from "../../shared/SessionStorage";
|
2017-12-24 04:02:57 -05:00
|
|
|
import { AdminApiService } from "../../shared/services/admin/admin-api.service";
|
|
|
|
import { IntegrationsApiService } from "../../shared/services/integrations/integrations-api.service";
|
2021-08-16 18:00:59 -04:00
|
|
|
import {
|
2021-09-01 19:01:01 -04:00
|
|
|
ConfigSimpleBotComponent,
|
|
|
|
SimpleBotConfigDialogContext,
|
2021-08-16 18:00:59 -04:00
|
|
|
} from "../../configs/simple-bot/simple-bot.component";
|
2018-03-31 18:47:30 -04:00
|
|
|
import { ToasterService } from "angular2-toaster";
|
2018-05-13 01:51:58 -04:00
|
|
|
import { StickerApiService } from "../../shared/services/integrations/sticker-api.service";
|
2020-10-23 07:30:20 -04:00
|
|
|
import { TranslateService } from "@ngx-translate/core";
|
2021-08-16 18:00:59 -04:00
|
|
|
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
|
2017-12-15 01:41:56 -05:00
|
|
|
|
|
|
|
const CATEGORY_MAP = {
|
2021-09-01 19:01:01 -04:00
|
|
|
Widgets: ["widget"],
|
|
|
|
Bots: ["complex-bot", "bot"],
|
|
|
|
Bridges: ["bridge"],
|
2017-12-15 01:41:56 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
@Component({
|
2021-09-01 19:29:24 -04:00
|
|
|
selector: "my-riot-home",
|
2021-09-01 19:01:01 -04:00
|
|
|
templateUrl: "./home.component.html",
|
|
|
|
styleUrls: ["./home.component.scss"],
|
2017-12-15 01:41:56 -05:00
|
|
|
})
|
|
|
|
export class RiotHomeComponent {
|
2021-09-01 19:01:01 -04:00
|
|
|
public isLoading = true;
|
|
|
|
public isError = false;
|
|
|
|
public errorMessage: string;
|
|
|
|
public isRoomEncrypted: boolean;
|
|
|
|
public hasStickerPacks = false;
|
|
|
|
|
|
|
|
private roomId: string;
|
|
|
|
private userId: string;
|
|
|
|
private requestedScreen: string = null;
|
|
|
|
private requestedIntegrationId: string = null;
|
|
|
|
public integrationsForCategory: { [category: string]: FE_Integration[] } = {};
|
|
|
|
private categoryMap: { [categoryName: string]: string[] } = CATEGORY_MAP;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private activatedRoute: ActivatedRoute,
|
|
|
|
private scalarApi: ScalarServerApiService,
|
|
|
|
private scalar: ScalarClientApiService,
|
|
|
|
private integrationsApi: IntegrationsApiService,
|
|
|
|
private stickerApi: StickerApiService,
|
|
|
|
private adminApi: AdminApiService,
|
|
|
|
private router: Router,
|
|
|
|
private modal: NgbModal,
|
|
|
|
private toaster: ToasterService,
|
|
|
|
public translate: TranslateService
|
|
|
|
) {
|
|
|
|
this.translate = translate;
|
|
|
|
const params: any = this.activatedRoute.snapshot.queryParams;
|
|
|
|
|
|
|
|
this.requestedScreen = params.screen;
|
|
|
|
this.requestedIntegrationId = params.integ_id;
|
|
|
|
|
|
|
|
if (SessionStorage.roomId && SessionStorage.userId) {
|
|
|
|
this.roomId = SessionStorage.roomId;
|
|
|
|
this.userId = SessionStorage.userId;
|
|
|
|
console.log(
|
|
|
|
"Already checked scalar token and other params - continuing startup"
|
|
|
|
);
|
|
|
|
this.prepareIntegrations();
|
|
|
|
return;
|
|
|
|
}
|
2017-12-22 23:08:10 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
if (!params.scalar_token || !params.room_id) {
|
2021-08-16 18:00:59 -04:00
|
|
|
console.error(
|
2021-09-01 19:01:01 -04:00
|
|
|
"Unable to load Dimension. Missing room ID or scalar token."
|
2021-08-16 18:00:59 -04:00
|
|
|
);
|
2017-12-15 01:41:56 -05:00
|
|
|
this.isError = true;
|
|
|
|
this.isLoading = false;
|
2021-08-16 18:00:59 -04:00
|
|
|
this.translate
|
2021-09-01 19:01:01 -04:00
|
|
|
.get("Unable to load Dimension - missing room ID or token.")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
this.errorMessage = res;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.roomId = params.room_id;
|
|
|
|
SessionStorage.scalarToken = params.scalar_token;
|
|
|
|
SessionStorage.roomId = this.roomId;
|
|
|
|
|
|
|
|
this.scalarApi
|
|
|
|
.getAccount()
|
|
|
|
.then((response) => {
|
|
|
|
const userId = response.user_id;
|
|
|
|
SessionStorage.userId = userId;
|
|
|
|
if (!userId) {
|
|
|
|
console.error(
|
|
|
|
"No user returned for token. Is the token registered in Dimension?"
|
|
|
|
);
|
|
|
|
this.isError = true;
|
|
|
|
this.isLoading = false;
|
|
|
|
this.translate
|
|
|
|
.get(
|
|
|
|
"Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!"
|
|
|
|
)
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
this.errorMessage = res;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.userId = userId;
|
|
|
|
console.log("Scalar token belongs to " + userId);
|
|
|
|
this.checkAdmin();
|
|
|
|
this.prepareIntegrations();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(err);
|
|
|
|
this.isError = true;
|
|
|
|
this.isLoading = false;
|
|
|
|
this.translate
|
|
|
|
.get(
|
|
|
|
"Unable to communicate with Dimension due to an unknown error."
|
|
|
|
)
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
this.errorMessage = res;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2017-12-15 01:41:56 -05:00
|
|
|
}
|
2021-08-16 18:00:59 -04:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
private checkAdmin() {
|
|
|
|
this.adminApi
|
|
|
|
.isAdmin()
|
|
|
|
.then(() => {
|
|
|
|
console.log(
|
|
|
|
SessionStorage.userId + " is an admin for this Dimension instance"
|
|
|
|
);
|
|
|
|
SessionStorage.isAdmin = true;
|
|
|
|
})
|
|
|
|
.catch(() => (SessionStorage.isAdmin = false));
|
2017-12-23 20:47:41 -05:00
|
|
|
}
|
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
public hasIntegrations(): boolean {
|
|
|
|
for (const category of this.getCategories()) {
|
|
|
|
if (this.getIntegrationsIn(category).length > 0) return true;
|
|
|
|
}
|
2017-12-15 01:41:56 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
return false;
|
|
|
|
}
|
2017-12-15 01:41:56 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
public getCategories(): string[] {
|
|
|
|
return Object.keys(this.categoryMap);
|
|
|
|
}
|
2021-08-16 18:00:59 -04:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
public getIntegrationsIn(category: string): FE_Integration[] {
|
|
|
|
return this.integrationsForCategory[category];
|
2017-12-15 01:41:56 -05:00
|
|
|
}
|
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
private getIntegrations(): FE_Integration[] {
|
|
|
|
const result: FE_Integration[] = [];
|
|
|
|
|
|
|
|
for (const category of this.getCategories()) {
|
|
|
|
for (const integration of this.getIntegrationsIn(category)) {
|
|
|
|
result.push(integration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
2021-08-16 18:00:59 -04:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
public modifyIntegration(integration: FE_Integration) {
|
|
|
|
if (!integration._isSupported) {
|
|
|
|
console.log(
|
|
|
|
this.userId +
|
2021-08-16 18:00:59 -04:00
|
|
|
" tried to modify " +
|
|
|
|
integration.displayName +
|
|
|
|
" with error: " +
|
|
|
|
integration._notSupportedReason
|
2021-09-01 19:01:01 -04:00
|
|
|
);
|
|
|
|
this.translate
|
|
|
|
.get(
|
|
|
|
"You do not appear to have permission to modify widgets in this room"
|
|
|
|
)
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
const reason =
|
2021-08-16 18:00:59 -04:00
|
|
|
integration.category === "widget"
|
2021-09-01 19:01:01 -04:00
|
|
|
? res
|
|
|
|
: integration._notSupportedReason;
|
|
|
|
this.toaster.pop("error", reason);
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
SessionStorage.editIntegration = integration;
|
|
|
|
SessionStorage.editsRequested++;
|
|
|
|
console.log(
|
|
|
|
this.userId + " is trying to modify " + integration.displayName
|
|
|
|
);
|
2017-12-20 23:28:43 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
if (integration.category === "bot") {
|
|
|
|
const widgetConfigRef = this.modal.open(ConfigSimpleBotComponent, {
|
|
|
|
backdrop: "static",
|
|
|
|
size: "lg",
|
|
|
|
});
|
|
|
|
const widgetConfigInterface =
|
2021-08-16 18:00:59 -04:00
|
|
|
widgetConfigRef.componentInstance as SimpleBotConfigDialogContext;
|
2021-09-01 19:01:01 -04:00
|
|
|
widgetConfigInterface.bot = <FE_SimpleBot>integration;
|
|
|
|
widgetConfigInterface.roomId = this.roomId;
|
|
|
|
} else {
|
|
|
|
console.log(
|
|
|
|
"Navigating to edit screen for " +
|
2021-08-16 18:00:59 -04:00
|
|
|
integration.category +
|
|
|
|
" " +
|
|
|
|
integration.type
|
2021-09-01 19:01:01 -04:00
|
|
|
);
|
|
|
|
this.router.navigate(
|
|
|
|
["riot-app", integration.category, integration.type],
|
|
|
|
{ queryParams: { roomId: this.roomId } }
|
|
|
|
);
|
|
|
|
}
|
2021-08-16 18:00:59 -04:00
|
|
|
}
|
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
private prepareIntegrations() {
|
|
|
|
this.scalar
|
|
|
|
.isRoomEncrypted(this.roomId)
|
|
|
|
.then((payload) => {
|
|
|
|
this.isRoomEncrypted = payload.response;
|
|
|
|
return this.integrationsApi.getIntegrations(this.roomId);
|
|
|
|
})
|
|
|
|
.then((response) => {
|
|
|
|
const integrations: FE_Integration[] = _.flatten(
|
|
|
|
Object.keys(response).map((k) => response[k])
|
|
|
|
);
|
|
|
|
const supportedIntegrations: FE_Integration[] = _.filter(
|
|
|
|
integrations,
|
|
|
|
(i) => IntegrationsRegistry.isSupported(i)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Flag integrations that aren't supported in encrypted rooms
|
|
|
|
if (this.isRoomEncrypted) {
|
|
|
|
for (const integration of supportedIntegrations) {
|
|
|
|
if (!integration.isEncryptionSupported) {
|
|
|
|
integration._isSupported = false;
|
|
|
|
integration._notSupportedReason =
|
2021-08-16 18:00:59 -04:00
|
|
|
"This integration is not supported in encrypted rooms";
|
2021-09-01 19:01:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up the categories
|
|
|
|
for (const category of Object.keys(this.categoryMap)) {
|
|
|
|
const supportedTypes = this.categoryMap[category];
|
|
|
|
this.integrationsForCategory[category] = _.filter(
|
|
|
|
supportedIntegrations,
|
|
|
|
(i) => supportedTypes.indexOf(i.category) !== -1
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const promises = supportedIntegrations.map((i) =>
|
|
|
|
this.updateIntegrationState(i)
|
|
|
|
);
|
|
|
|
return Promise.all(promises);
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
this.isLoading = false;
|
|
|
|
|
|
|
|
// HACK: We wait for the digest cycle so we actually have components to look at
|
|
|
|
setTimeout(() => this.tryOpenConfigScreen(), 20);
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(err);
|
|
|
|
this.isError = true;
|
|
|
|
this.isLoading = false;
|
|
|
|
this.translate
|
|
|
|
.get(
|
|
|
|
"Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server."
|
|
|
|
)
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
this.errorMessage = res;
|
|
|
|
});
|
|
|
|
});
|
2017-12-20 23:28:43 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
this.stickerApi
|
|
|
|
.getPacks()
|
|
|
|
.then((packs) => {
|
|
|
|
this.hasStickerPacks = packs.length > 0;
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private tryOpenConfigScreen() {
|
|
|
|
let category = null;
|
|
|
|
let type = null;
|
|
|
|
if (!this.requestedScreen) return;
|
|
|
|
|
|
|
|
if (this.requestedScreen === "type_m.stickerpicker") {
|
|
|
|
console.log(
|
|
|
|
"Intercepting config screen handling to open sticker picker config"
|
|
|
|
);
|
|
|
|
this.router.navigate(["riot-app", "stickerpicker"]);
|
|
|
|
return;
|
2018-03-31 18:47:30 -04:00
|
|
|
}
|
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
const targetIntegration = IntegrationsRegistry.getIntegrationForScreen(
|
|
|
|
this.requestedScreen
|
2021-08-16 18:00:59 -04:00
|
|
|
);
|
2021-09-01 19:01:01 -04:00
|
|
|
if (targetIntegration) {
|
|
|
|
category = targetIntegration.category;
|
|
|
|
type = targetIntegration.type;
|
|
|
|
} else {
|
|
|
|
console.log("Unknown screen requested: " + this.requestedScreen);
|
|
|
|
}
|
2017-12-15 01:41:56 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
console.log("Searching for integration for requested screen");
|
|
|
|
for (const integration of this.getIntegrations()) {
|
|
|
|
if (integration.category === category && integration.type === type) {
|
|
|
|
console.log(
|
|
|
|
"Configuring integration " +
|
2021-08-16 18:00:59 -04:00
|
|
|
this.requestedIntegrationId +
|
|
|
|
" category=" +
|
|
|
|
category +
|
|
|
|
" type=" +
|
|
|
|
type
|
2021-09-01 19:01:01 -04:00
|
|
|
);
|
|
|
|
SessionStorage.editIntegration = integration;
|
|
|
|
SessionStorage.editIntegrationId = this.requestedIntegrationId;
|
|
|
|
this.modifyIntegration(integration);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2018-05-13 01:51:58 -04:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
console.log(
|
|
|
|
"Failed to find integration component for category=" +
|
2021-08-16 18:00:59 -04:00
|
|
|
category +
|
|
|
|
" type=" +
|
|
|
|
type
|
2021-09-01 19:01:01 -04:00
|
|
|
);
|
2017-12-15 01:41:56 -05:00
|
|
|
}
|
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
private async updateIntegrationState(integration: FE_Integration) {
|
|
|
|
if (!integration.isOnline) {
|
|
|
|
integration._isSupported = false;
|
|
|
|
this.translate
|
|
|
|
.get("This integration is offline or unavailable")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
integration._notSupportedReason = res;
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!integration.requirements) return;
|
2021-08-16 18:00:59 -04:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
const promises = integration.requirements.map((r) =>
|
|
|
|
this.checkRequirement(r)
|
|
|
|
);
|
2021-08-16 18:00:59 -04:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
if (integration.category === "bot") {
|
|
|
|
const state = await this.scalar.getMembershipState(
|
|
|
|
this.roomId,
|
|
|
|
(<FE_SimpleBot>integration).userId
|
|
|
|
);
|
|
|
|
if (state && state.response && state.response.membership) {
|
|
|
|
integration._inRoom =
|
2021-08-16 18:00:59 -04:00
|
|
|
["join", "invite"].indexOf(state.response.membership) !== -1;
|
2021-09-01 19:01:01 -04:00
|
|
|
} else integration._inRoom = false;
|
|
|
|
}
|
2017-12-15 01:41:56 -05:00
|
|
|
|
2021-09-01 19:01:01 -04:00
|
|
|
return Promise.all(promises).then(
|
|
|
|
() => {
|
|
|
|
integration._isSupported = true;
|
|
|
|
integration._notSupportedReason = null;
|
|
|
|
},
|
|
|
|
(error) => {
|
|
|
|
console.error(error);
|
|
|
|
integration._isSupported = false;
|
|
|
|
integration._notSupportedReason = error;
|
2021-08-16 18:00:59 -04:00
|
|
|
}
|
2021-09-01 19:01:01 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private checkRequirement(requirement: FE_IntegrationRequirement) {
|
|
|
|
switch (requirement.condition) {
|
|
|
|
case "publicRoom":
|
|
|
|
return this.scalar.getJoinRule(this.roomId).then((payload) => {
|
|
|
|
if (!payload.response) {
|
|
|
|
let message: string;
|
|
|
|
this.translate
|
|
|
|
.get("Could not communicate with Element")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res;
|
|
|
|
});
|
|
|
|
return Promise.reject(message);
|
|
|
|
}
|
|
|
|
const isPublic = payload.response.join_rule === "public";
|
|
|
|
if (isPublic !== requirement.expectedValue) {
|
|
|
|
let message: string;
|
|
|
|
let message1: string;
|
|
|
|
this.translate
|
|
|
|
.get(["The room must be", "to use this integration"])
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res[0];
|
|
|
|
message1 = res[1];
|
|
|
|
});
|
|
|
|
return Promise.reject(
|
|
|
|
message + (isPublic ? "non-public" : "public") + message1
|
|
|
|
);
|
|
|
|
} else return Promise.resolve();
|
2017-12-20 23:28:43 -05:00
|
|
|
});
|
2021-09-01 19:01:01 -04:00
|
|
|
case "canSendEventTypes":
|
|
|
|
const processPayload = (payload) => {
|
|
|
|
const response = payload.response;
|
|
|
|
if (response === true) return Promise.resolve();
|
|
|
|
if (response.error || response.error.message) {
|
|
|
|
let message: string;
|
|
|
|
this.translate
|
|
|
|
.get("You cannot modify widgets in this room")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res;
|
|
|
|
});
|
|
|
|
return Promise.reject(message);
|
|
|
|
}
|
|
|
|
let message: string;
|
|
|
|
this.translate
|
|
|
|
.get("Error communicating with Element")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res;
|
|
|
|
});
|
|
|
|
return Promise.reject(message);
|
|
|
|
};
|
|
|
|
|
|
|
|
let promiseChain = Promise.resolve();
|
|
|
|
requirement.argument.forEach(
|
|
|
|
(e) =>
|
|
|
|
(promiseChain = promiseChain.then(() =>
|
|
|
|
this.scalar
|
|
|
|
.canSendEvent(this.roomId, e.type, e.isState)
|
|
|
|
.then(processPayload)
|
|
|
|
.catch(processPayload)
|
|
|
|
))
|
|
|
|
);
|
|
|
|
return promiseChain
|
|
|
|
.then(() => {
|
|
|
|
if (!requirement.expectedValue) {
|
|
|
|
let message: string;
|
|
|
|
this.translate
|
|
|
|
.get("Expected to not be able to send specific event types")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res;
|
|
|
|
});
|
|
|
|
return Promise.reject(message);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(err);
|
|
|
|
if (requirement.expectedValue) {
|
|
|
|
let message: string;
|
|
|
|
this.translate
|
|
|
|
.get("Expected to be able to send specific event types")
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res;
|
|
|
|
});
|
|
|
|
return Promise.reject(message);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
case "userInRoom":
|
|
|
|
// TODO: Implement
|
|
|
|
default:
|
|
|
|
let message: string;
|
|
|
|
let message1: string;
|
|
|
|
this.translate
|
|
|
|
.get(["Requirement", "not found"])
|
|
|
|
.subscribe((res: string) => {
|
|
|
|
message = res[0];
|
|
|
|
message1 = res[1];
|
|
|
|
});
|
|
|
|
return Promise.reject(message + requirement.condition + message1);
|
|
|
|
}
|
2017-12-15 01:41:56 -05:00
|
|
|
}
|
|
|
|
}
|