Re-wire the UI to support the new backend

This still doesn't allow editing, but it supports showing the widgets at least.
This commit is contained in:
Travis Ralston 2017-12-20 21:28:43 -07:00
parent 599fb80112
commit 4965b61f2d
40 changed files with 373 additions and 274 deletions

View File

@ -1,4 +1,4 @@
import { GET, Path, QueryParam } from "typescript-rest";
import { GET, Path, PathParam, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
import { ScalarService } from "../scalar/ScalarService";
import { DimensionStore } from "../../db/DimensionStore";
@ -35,6 +35,13 @@ export class DimensionIntegrationsService {
});
}
@GET
@Path("room/:roomId")
public getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string) :Promise<IntegrationsResponse>{
console.log(roomId);
return this.getEnabledIntegrations(scalarToken);
}
private getIntegrations(isEnabledCheck?: boolean): Promise<IntegrationsResponse> {
const cachedResponse = DimensionIntegrationsService.integrationCache.get("integrations_" + isEnabledCheck);
if (cachedResponse) {

View File

@ -12,6 +12,7 @@ export default {
"avatarUrl": {type: DataType.STRING, allowNull: false},
"description": {type: DataType.STRING, allowNull: false},
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
"isPublic": {type: DataType.BOOLEAN, allowNull: false},
"optionsJson": {type: DataType.STRING, allowNull: true},
}))
.then(() => queryInterface.bulkInsert("dimension_widgets", [
@ -64,6 +65,15 @@ export default {
avatarUrl: "/img/avatars/twitch.png",
description: "Embed a Twitch livestream into your room.",
},
{
type: "jitsi",
name: "Jitsi Conference",
isEnabled: true,
isPublic: true,
avatarUrl: "/img/avatars/jitsi.png",
description: "Hold a video conference with Jitsi Meet",
optionsJson: '{"jitsiDomain":"jitsi.riot.im", "scriptUrl":"https://jitsi.riot.im/libs/external_api.min.js"}',
},
]));
},
down: (queryInterface: QueryInterface) => {

View File

@ -5,6 +5,7 @@ export class Integration {
public category: "bot" | "complex-bot" | "bridge" | "widget";
public type: string;
public requirements: IntegrationRequirement[];
public isEncryptionSupported = false;
// These are meant to be set by us
public displayName: string;
@ -26,5 +27,9 @@ export class Integration {
export interface IntegrationRequirement {
condition: "publicRoom" | "canSendEventTypes";
argument: any;
// For publicRoom this is true or false (boolean)
// For canSendEventTypes this is an array of {isState: boolean, type: string}
// For userInRoom this is the user ID
expectedValue: any;
}

View File

@ -5,6 +5,11 @@ export interface EtherpadWidgetOptions {
defaultUrl: string;
}
export interface JitsiWidgetOptions {
jitsiDomain: string;
scriptUrl: string;
}
export class Widget extends Integration {
public options: any;
@ -14,8 +19,11 @@ export class Widget extends Integration {
this.options = widgetRecord.optionsJson ? JSON.parse(widgetRecord.optionsJson) : {};
this.requirements = [{
condition: "canSendEventTypes",
argument: ["im.vector.widget"],
argument: [{isState: true, type: "im.vector.widget"}],
expectedValue: true,
}];
// Technically widgets are supported in encrypted rooms, although at risk.
this.isEncryptionSupported = true;
}
}

View File

@ -14,6 +14,8 @@ export class MatrixOpenIdClient {
"/_matrix/federation/v1/openid/userinfo",
{access_token: this.openId.access_token}
).then(response => {
// Annoyingly, the response isn't JSON for this
response = JSON.parse(response);
return response['sub'];
});
}

View File

@ -8,16 +8,14 @@ import { routing } from "./app.routing";
import { createNewHosts, removeNgStyles } from "@angularclass/hmr";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { RiotComponent } from "./riot/riot.component";
import { ApiService } from "./shared/services/api.service";
import { UiSwitchModule } from "angular2-ui-switch";
import { ScalarService } from "./shared/services/scalar.service";
import { ScalarClientApiService } from "./shared/services/scalar-client-api.service";
import { ToasterModule } from "angular2-toaster";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ScalarCloseComponent } from "./riot/scalar-close/scalar-close.component";
import { IntegrationService } from "./shared/services/integration.service";
import { LegacyIntegrationService } from "./shared/services/legacy/integration.service";
import { BootstrapModalModule } from "ngx-modialog/plugins/bootstrap";
import { ModalModule } from "ngx-modialog";
import { IrcApiService } from "./shared/services/irc-api.service";
import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic.component";
import { ToggleFullscreenDirective } from "./shared/directives/toggle-fullscreen.directive";
import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button.component";
@ -30,8 +28,11 @@ import { BreadcrumbsModule } from "ng2-breadcrumbs";
import { RiotHomeComponent } from "./riot/riot-home/home.component";
import { IntegrationBagComponent } from "./integration-bag/integration-bag.component";
import { IntegrationComponent } from "./integration/integration.component";
import { ScalarServerApiService } from "./shared/services/scalar-server-api.service";
import { DimensionApiService } from "./shared/services/dimension-api.service";
import { AdminApiService } from "./shared/services/admin-api.service";
const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigComponents();
const WIDGET_CONFIGURATION_COMPONENTS: any[] = LegacyIntegrationService.getAllConfigComponents();
@NgModule({
imports: [
@ -68,10 +69,10 @@ const WIDGET_CONFIGURATION_COMPONENTS: any[] = IntegrationService.getAllConfigCo
// Vendor
],
providers: [
ApiService,
ScalarService,
IntegrationService,
IrcApiService,
ScalarClientApiService,
ScalarServerApiService,
DimensionApiService,
AdminApiService,
{provide: Window, useValue: window},
// Vendor

View File

@ -1,9 +1,9 @@
import { Component } from "@angular/core";
import { CircleCiIntegration } from "../../shared/models/integration";
import { CircleCiIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { ApiService } from "../../shared/services/api.service";
import { ApiService } from "../../shared/services/legacy/api.service";
@Component({
selector: "my-circleci-config",

View File

@ -1,11 +1,11 @@
import { Component, OnDestroy } from "@angular/core";
import { IRCIntegration } from "../../shared/models/integration";
import { IRCIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { IrcApiService } from "../../shared/services/irc-api.service";
import { IrcApiService } from "../../shared/services/legacy/irc-api.service";
import { ToasterService } from "angular2-toaster";
import { IntervalObservable } from "rxjs/observable/IntervalObservable";
import { ApiService } from "../../shared/services/api.service";
import { ApiService } from "../../shared/services/legacy/api.service";
import { Subscription } from "rxjs";
@Component({

View File

@ -1,9 +1,9 @@
import { Component } from "@angular/core";
import { RSSIntegration } from "../../shared/models/integration";
import { RSSIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { ApiService } from "../../shared/services/api.service";
import { ApiService } from "../../shared/services/legacy/api.service";
@Component({
selector: "my-rss-config",

View File

@ -1,9 +1,9 @@
import { Component } from "@angular/core";
import { TravisCiIntegration } from "../../shared/models/integration";
import { TravisCiIntegration } from "../../shared/models/legacyintegration";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { ConfigModalContext } from "../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { ApiService } from "../../shared/services/api.service";
import { ApiService } from "../../shared/services/legacy/api.service";
@Component({
selector: "my-travisci-config",

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_CUSTOM } from "../../../shared/models/widget";
@ -15,7 +15,7 @@ export class CustomWidgetConfigComponent extends WidgetComponent implements Moda
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,11 +1,11 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_ETHERPAD } from "../../../shared/models/widget";
import { EtherpadWidgetIntegration } from "../../../shared/models/integration";
import { EtherpadWidgetIntegration } from "../../../shared/models/legacyintegration";
@Component({
selector: "my-etherpadwidget-config",
@ -18,7 +18,7 @@ export class EtherpadWidgetConfigComponent extends WidgetComponent implements Mo
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_GOOGLE_CALENDAR } from "../../../shared/models/widget";
@ -15,7 +15,7 @@ export class GoogleCalendarWidgetConfigComponent extends WidgetComponent impleme
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { WIDGET_GOOGLE_DOCS } from "../../../shared/models/widget";
@ -15,7 +15,7 @@ export class GoogleDocsWidgetConfigComponent extends WidgetComponent implements
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,11 +1,11 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_JITSI } from "../../../shared/models/widget";
import { JitsiWidgetIntegration } from "../../../shared/models/integration";
import { JitsiWidgetIntegration } from "../../../shared/models/legacyintegration";
import * as gobyInit from "goby";
import * as url from "url";
@ -26,7 +26,7 @@ export class JitsiWidgetConfigComponent extends WidgetComponent implements Modal
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_TWITCH } from "../../../shared/models/widget";
@ -15,7 +15,7 @@ export class TwitchWidgetConfigComponent extends WidgetComponent implements Moda
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,7 +1,7 @@
import { ScalarService } from "../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../shared/services/scalar-client-api.service";
import { convertScalarWidgetsToDtos, EditableWidget } from "../../shared/models/widget";
import { ToasterService } from "angular2-toaster";
import { Integration } from "../../shared/models/integration";
import { LegacyIntegration } from "../../shared/models/legacyintegration";
const SCALAR_WIDGET_LINKS = [
"https://scalar-staging.riot.im/scalar/api/widgets/__TYPE__.html?url=",
@ -23,9 +23,9 @@ export class WidgetComponent {
constructor(window: Window,
protected toaster: ToasterService,
protected scalarApi: ScalarService,
protected scalarApi: ScalarClientApiService,
public roomId: string,
public integration: Integration,
public integration: LegacyIntegration,
editWidgetId: string,
private widgetIds: string[],
private defaultName: string,

View File

@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../../shared/services/scalar-client-api.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { EditableWidget, WIDGET_YOUTUBE } from "../../../shared/models/widget";
@ -17,7 +17,7 @@ export class YoutubeWidgetConfigComponent extends WidgetComponent implements Mod
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
scalarService: ScalarClientApiService,
window: Window) {
super(
window,

View File

@ -1,10 +1,8 @@
<div class="integration-bag">
<!--<my-integration *ngFor="let integration of integrations"-->
<!--[integration]="integration" (selected)="onClick(integration)"></my-integration>-->
<div class="integration" *ngFor="let integration of integrations">
<img class="integration-avatar" [src]="getSafeUrl(integration.avatar)"/>
<div class="integration-name">{{ integration.name }}</div>
<div class="integration-description">{{ integration.about }}</div>
<img class="integration-avatar" [src]="getSafeUrl(integration.avatarUrl)"/>
<div class="integration-name">{{ integration.displayName }}</div>
<div class="integration-description">{{ integration.description }}</div>
<div class="integration-arrow"><i class="fa fa-chevron-right"></i></div>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Integration } from "../shared/models/integration";
import { LegacyIntegration } from "../shared/models/legacyintegration";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
import { Integration } from "../shared/models/integration";
@Component({
selector: "my-integration-bag",
@ -9,7 +10,7 @@ import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
})
export class IntegrationBagComponent {
@Input() integrations: Integration[];
@Input() integrations: LegacyIntegration[];
@Output() integrationClicked: EventEmitter<Integration> = new EventEmitter<Integration>();
constructor(private sanitizer: DomSanitizer) {

View File

@ -1,10 +1,10 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Integration } from "../shared/models/integration";
import { LegacyIntegration } from "../shared/models/legacyintegration";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
export class ConfigModalContext extends BSModalContext {
public integration: Integration;
public integration: LegacyIntegration;
public roomId: string;
public userId: string;
public scalarToken: string;
@ -18,7 +18,7 @@ export class ConfigModalContext extends BSModalContext {
})
export class IntegrationComponent {
@Input() integration: Integration;
@Input() integration: LegacyIntegration;
@Output() selected: EventEmitter<any> = new EventEmitter<any>();
constructor(private sanitizer: DomSanitizer) {

View File

@ -1,12 +1,13 @@
import { Component, ViewChildren } from "@angular/core";
import { IntegrationService } from "../../shared/services/integration.service";
import { IntegrationComponent } from "../../integration/integration.component";
import { Component } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { Integration } from "../../shared/models/integration";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "../../shared/services/api.service";
import { ScalarService } from "../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../shared/services/scalar-client-api.service";
import * as _ from "lodash";
import { ScalarServerApiService } from "../../shared/services/scalar-server-api.service";
import { AuthedApi } from "../../shared/services/AuthedApi";
import { DimensionApiService } from "../../shared/services/dimension-api.service";
import { Integration, IntegrationRequirement } from "../../shared/models/integration";
import { IntegrationService } from "../../shared/services/integration.service";
const CATEGORY_MAP = {
"Widgets": ["widget"],
@ -20,14 +21,11 @@ const CATEGORY_MAP = {
styleUrls: ["./home.component.scss"],
})
export class RiotHomeComponent {
@ViewChildren(IntegrationComponent) integrationComponents: Array<IntegrationComponent>;
public isLoading = true;
public isError = false;
public errorMessage: string;
public isRoomEncrypted: boolean;
private scalarToken: string;
private roomId: string;
private userId: string;
private requestedScreen: string = null;
@ -36,8 +34,9 @@ export class RiotHomeComponent {
private categoryMap: { [categoryName: string]: string[] } = CATEGORY_MAP;
constructor(private activatedRoute: ActivatedRoute,
private api: ApiService,
private scalar: ScalarService,
private scalarApi: ScalarServerApiService,
private scalar: ScalarClientApiService,
private dimensionApi: DimensionApiService,
private toaster: ToasterService) {
let params: any = this.activatedRoute.snapshot.queryParams;
@ -51,9 +50,10 @@ export class RiotHomeComponent {
this.errorMessage = "Unable to load Dimension - missing room ID or token.";
} else {
this.roomId = params.room_id;
this.scalarToken = params.scalar_token;
AuthedApi.SCALAR_TOKEN = params.scalar_token;
this.api.getTokenOwner(params.scalar_token).then(userId => {
this.scalarApi.getAccount().then(response => {
const userId = response.user_id;
if (!userId) {
console.error("No user returned for token. Is the token registered in Dimension?");
this.isError = true;
@ -89,31 +89,42 @@ export class RiotHomeComponent {
return this.integrationsForCategory[category];
}
public modifyIntegration(integration: Integration) {
console.log(this.userId + " is trying to modify " + integration.name);
private getIntegrations(): Integration[] {
const result: Integration[] = [];
if (integration.hasAdditionalConfig) {
// TODO: Navigate to edit screen
console.log("EDIT SCREEN FOR " + integration.name);
} else {
// It's a flip-a-bit (simple bot)
for (const category of this.getCategories()) {
for (const integration of this.getIntegrationsIn(category)) {
result.push(integration);
}
}
return result;
}
public modifyIntegration(integration: Integration) {
console.log(this.userId + " is trying to modify " + integration.displayName);
if (integration.category === "bot") {
// It's a bot
// TODO: "Are you sure?" dialog
let promise = null;
if (!integration.isEnabled) {
promise = this.scalar.inviteUser(this.roomId, integration.userId);
} else promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken);
// let promise = null;
const promise = Promise.resolve();
// if (!integration._inRoom) {
// promise = this.scalar.inviteUser(this.roomId, integration.userId);
// } else promise = this.api.removeIntegration(this.roomId, integration.type, integration.integrationType, this.scalarToken);
// We set this ahead of the promise for debouncing
integration.isEnabled = !integration.isEnabled;
integration.isUpdating = true;
integration._inRoom = !integration._inRoom;
integration._isUpdating = true;
promise.then(() => {
integration.isUpdating = false;
if (integration.isEnabled) this.toaster.pop("success", integration.name + " was invited to the room");
else this.toaster.pop("success", integration.name + " was removed from the room");
integration._isUpdating = false;
if (integration._inRoom) this.toaster.pop("success", integration.displayName + " was invited to the room");
else this.toaster.pop("success", integration.displayName + " was removed from the room");
}).catch(err => {
integration.isEnabled = !integration.isEnabled; // revert the status change
integration.isUpdating = false;
integration._inRoom = !integration._inRoom; // revert the status change
integration._isUpdating = false;
console.error(err);
let errorMessage = null;
@ -122,31 +133,27 @@ export class RiotHomeComponent {
if (!errorMessage) errorMessage = "Could not update integration status";
this.toaster.pop("error", errorMessage);
})
});
} else {
// TODO: Navigate to edit screen
console.log("EDIT SCREEN FOR " + integration.displayName);
}
}
private prepareIntegrations() {
this.scalar.isRoomEncrypted(this.roomId).then(payload => {
this.isRoomEncrypted = payload.response;
return this.api.getIntegrations(this.roomId, this.scalarToken);
}).then(integrations => {
return this.dimensionApi.getIntegrations(this.roomId);
}).then(response => {
const integrations: Integration[] = _.flatten(Object.keys(response).map(k => response[k]));
const supportedIntegrations: Integration[] = _.filter(integrations, i => IntegrationService.isSupported(i));
for (const integration of supportedIntegrations) {
// Widgets technically support encrypted rooms, so unless they explicitly declare that
// they don't, we'll assume they do. A warning about adding widgets in encrypted rooms
// is displayed to users elsewhere.
if (integration.type === "widget" && integration.supportsEncryptedRooms !== false)
integration.supportsEncryptedRooms = true;
}
// Flag integrations that aren't supported in encrypted rooms
if (this.isRoomEncrypted) {
for (const integration of supportedIntegrations) {
if (!integration.supportsEncryptedRooms) {
integration.isSupported = false;
integration.notSupportedReason = "This integration is not supported in encrypted rooms";
if (!integration.isEncryptionSupported) {
integration._isSupported = false;
integration._notSupportedReason = "This integration is not supported in encrypted rooms";
}
}
}
@ -154,7 +161,7 @@ export class RiotHomeComponent {
// 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.type) !== -1);
this.integrationsForCategory[category] = _.filter(supportedIntegrations, i => supportedTypes.indexOf(i.category) !== -1);
}
let promises = supportedIntegrations.map(i => this.updateIntegrationState(i));
@ -173,92 +180,57 @@ export class RiotHomeComponent {
}
private tryOpenConfigScreen() {
let category = null;
let type = null;
let integrationType = null;
if (!this.requestedScreen) return;
const targetIntegration = IntegrationService.getIntegrationForScreen(this.requestedScreen);
if (targetIntegration) {
category = targetIntegration.category;
type = targetIntegration.type;
integrationType = targetIntegration.integrationType;
} else {
console.log("Unknown screen requested: " + this.requestedScreen);
}
let opened = false;
this.integrationComponents.forEach(component => {
if (opened) return;
if (component.integration.type !== type || component.integration.integrationType !== integrationType) return;
console.log("Configuring integration " + this.requestedIntegration + " type=" + type + " integrationType=" + integrationType);
//component.configureIntegration(this.requestedIntegration);
// TODO: Support editing integrations
opened = true;
});
if (!opened) {
console.log("Failed to find integration component for type=" + type + " integrationType=" + integrationType);
for (const integration of this.getIntegrations()) {
if (integration.category === category && integration.type === type) {
console.log("Configuring integration " + this.requestedIntegration + " category=" + category + " type=" + type);
// TODO: Actually edit integration
return;
}
}
console.log("Failed to find integration component for category=" + category + " type=" + type);
}
private updateIntegrationState(integration: Integration) {
integration.hasAdditionalConfig = IntegrationService.hasConfig(integration);
if (!integration.requirements) return;
if (integration.type === "widget") {
if (!integration.requirements) integration.requirements = {};
integration.requirements["canSetWidget"] = true;
}
let promises = integration.requirements.map(r => this.checkRequirement(r));
// If the integration has requirements, then we'll check those instead of anything else
if (integration.requirements) {
let keys = _.keys(integration.requirements);
let promises = [];
for (let key of keys) {
let requirement = this.checkRequirement(integration, key);
promises.push(requirement);
}
return Promise.all(promises).then(() => {
integration.isSupported = true;
integration.notSupportedReason = null;
}, error => {
console.error(error);
integration.isSupported = false;
integration.notSupportedReason = error;
});
}
// The integration doesn't have requirements, so we'll just make sure the bot user can be retrieved.
return this.scalar.getMembershipState(this.roomId, integration.userId).then(payload => {
if (payload.response) {
integration.isSupported = true;
integration.notSupportedReason = null;
integration.isEnabled = (payload.response.membership === "join" || payload.response.membership === "invite");
} else {
console.error("No response received to membership query of " + integration.userId);
integration.isSupported = false;
integration.notSupportedReason = "Unable to query membership state for this bot";
}
}, (error) => {
return Promise.all(promises).then(() => {
integration._isSupported = true;
integration._notSupportedReason = null;
}, error => {
console.error(error);
integration.isSupported = false;
integration.notSupportedReason = "Unable to query membership state for this bot";
integration._isSupported = false;
integration._notSupportedReason = error;
});
}
private checkRequirement(integration: Integration, key: string) {
let requirement = integration.requirements[key];
switch (key) {
case "joinRule":
private checkRequirement(requirement: IntegrationRequirement) {
switch (requirement.condition) {
case "publicRoom":
return this.scalar.getJoinRule(this.roomId).then(payload => {
if (!payload.response) {
return Promise.reject("Could not communicate with Riot");
}
return payload.response.join_rule === requirement
? Promise.resolve()
: Promise.reject("The room must be " + requirement + " to use this integration.");
const isPublic = payload.response.join_rule === "public";
if (isPublic !== requirement.expectedValue) {
return Promise.reject("The room must be " + (isPublic ? "non-public" : "public") + " to use this integration");
} else return Promise.resolve();
});
case "canSetWidget":
case "canSendEventTypes":
const processPayload = payload => {
const response = <any>payload.response;
if (response === true) return Promise.resolve();
@ -266,9 +238,17 @@ export class RiotHomeComponent {
return Promise.reject("You cannot modify widgets in this room");
return Promise.reject("Error communicating with Riot");
};
return this.scalar.canSendEvent(this.roomId, "im.vector.modular.widgets", true).then(processPayload).catch(processPayload);
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) return Promise.reject("Expected to not be able to send specific event types");
}).catch(err => {
console.error(err);
if (requirement.expectedValue) return Promise.reject("Expected to be able to send specific event types");
});
default:
return Promise.reject("Requirement '" + key + "' not found");
return Promise.reject("Requirement '" + requirement.condition + "' not found");
}
}
}

View File

@ -1,5 +1,5 @@
import { Component } from "@angular/core";
import { ScalarService } from "../../shared/services/scalar.service";
import { ScalarClientApiService } from "../../shared/services/scalar-client-api.service";
@Component({
selector: "my-scalar-close",
@ -8,7 +8,7 @@ import { ScalarService } from "../../shared/services/scalar.service";
})
export class ScalarCloseComponent {
constructor(private scalar: ScalarService) {
constructor(private scalar: ScalarClientApiService) {
}
public closeScalar() {

View File

@ -0,0 +1,5 @@
import { Widget } from "./integration";
export interface DimensionIntegrationsResponse {
widgets: Widget[];
}

View File

@ -1,49 +1,40 @@
export interface Integration {
// These are from the server
category: "bot" | "complex-bot" | "bridge" | "widget";
type: string;
integrationType: string;
userId: string;
name: string;
avatar: string;
about: string; // nullable
supportsEncryptedRooms: boolean;
requirements: any; // nullable
requirements: IntegrationRequirement[];
isEncryptionSupported: boolean;
displayName: string;
avatarUrl: string;
description: string;
isEnabled: boolean;
isPublic: boolean;
// These are set in the UI
isSupported: boolean;
notSupportedReason: string;
hasAdditionalConfig: boolean;
isEnabled: boolean; // for the flip-a-bit integrations
isUpdating: boolean;
// Used by us
_inRoom: boolean;
_isUpdating: boolean;
_isSupported: boolean;
_notSupportedReason: string;
}
export interface RSSIntegration extends Integration {
feeds: string[];
immutableFeeds: { url: string, ownerId: string }[];
export interface Widget extends Integration {
options: any;
}
export interface TravisCiIntegration extends Integration {
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
webhookUrl: string; // immutable
export interface EtherpadWidget extends Widget {
options: {
defaultUrl: string;
};
}
export interface CircleCiIntegration extends Integration {
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
webhookUrl: string; // immutable
export interface JitsiWidget extends Widget {
options: {
jitsiDomain: string;
scriptUrl: string;
};
}
export interface IRCIntegration extends Integration {
availableNetworks: { name: string, id: string }[];
channels: { [networkId: string]: string[] };
}
export interface EtherpadWidgetIntegration extends Integration {
defaultUrl: string;
}
export interface JitsiWidgetIntegration extends Integration {
jitsiDomain: string;
scriptUrl: string
export interface IntegrationRequirement {
condition: "publicRoom" | "canSendEventTypes";
argument: any;
expectedValue: any;
}

View File

@ -0,0 +1,49 @@
export interface LegacyIntegration {
// These are from the server
type: string;
integrationType: string;
userId: string;
name: string;
avatar: string;
about: string; // nullable
supportsEncryptedRooms: boolean;
requirements: any; // nullable
// These are set in the UI
isSupported: boolean;
notSupportedReason: string;
hasAdditionalConfig: boolean;
isEnabled: boolean; // for the flip-a-bit integrations
isUpdating: boolean;
}
export interface RSSIntegration extends LegacyIntegration {
feeds: string[];
immutableFeeds: { url: string, ownerId: string }[];
}
export interface TravisCiIntegration extends LegacyIntegration {
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
webhookUrl: string; // immutable
}
export interface CircleCiIntegration extends LegacyIntegration {
repoTemplates: { repoKey: string, template: string, newTemplate: string }[]; // newTemplate is local
immutableRepoTemplates: { repoKey: string, template: string, ownerId: string }[];
webhookUrl: string; // immutable
}
export interface IRCIntegration extends LegacyIntegration {
availableNetworks: { name: string, id: string }[];
channels: { [networkId: string]: string[] };
}
export interface EtherpadWidgetIntegration extends LegacyIntegration {
defaultUrl: string;
}
export interface JitsiWidgetIntegration extends LegacyIntegration {
jitsiDomain: string;
scriptUrl: string
}

View File

@ -0,0 +1,3 @@
export interface ScalarAccountResponse {
user_id: string;
}

View File

@ -1,4 +1,4 @@
import { WidgetsResponse } from "./scalar_responses";
import { WidgetsResponse } from "./scalar_client_responses";
export const WIDGET_CUSTOM = ["customwidget", "dimension-customwidget"];
export const WIDGET_ETHERPAD = ["etherpad", "dimension-etherpad"];

View File

@ -0,0 +1,15 @@
import { Http, Response } from "@angular/http";
import { Observable } from "rxjs/Observable";
export class AuthedApi {
public static SCALAR_TOKEN: string = null;
constructor(protected http: Http) {
}
protected authedGet(url: string, qs?: any): Observable<Response> {
if (!qs) qs = {};
qs["scalar_token"] = AuthedApi.SCALAR_TOKEN;
return this.http.get(url, {params: qs});
}
}

View File

@ -0,0 +1,10 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "./AuthedApi";
@Injectable()
export class AdminApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
}

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { AuthedApi } from "./AuthedApi";
import { DimensionIntegrationsResponse } from "../models/dimension_responses";
@Injectable()
export class DimensionApiService extends AuthedApi {
constructor(http: Http) {
super(http);
}
public getIntegrations(roomId: string): Promise<DimensionIntegrationsResponse> {
return this.authedGet("/api/v1/dimension/integrations/room/" + roomId).map(r => r.json()).toPromise();
}
}

View File

@ -1,21 +1,9 @@
import { Injectable } from "@angular/core";
import { Integration } from "../models/integration";
import { RssConfigComponent } from "../../configs/rss/rss-config.component";
import { ContainerContent } from "ngx-modialog";
import { IrcConfigComponent } from "../../configs/irc/irc-config.component";
import { TravisCiConfigComponent } from "../../configs/travisci/travisci-config.component";
import { CustomWidgetConfigComponent } from "../../configs/widget/custom_widget/custom_widget-config.component";
import { YoutubeWidgetConfigComponent } from "../../configs/widget/youtube/youtube-config.component";
import { TwitchWidgetConfigComponent } from "../../configs/widget/twitch/twitch-config.component";
import { EtherpadWidgetConfigComponent } from "../../configs/widget/etherpad/etherpad-config.component";
import { JitsiWidgetConfigComponent } from "../../configs/widget/jitsi/jitsi-config.component";
import { Component, Injectable } from "@angular/core";
import {
WIDGET_CUSTOM, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, WIDGET_JITSI, WIDGET_TWITCH,
WIDGET_YOUTUBE
} from "../models/widget";
import { GoogleDocsWidgetConfigComponent } from "../../configs/widget/googledocs/googledocs-config.component";
import { GoogleCalendarWidgetConfigComponent } from "../../configs/widget/googlecalendar/googlecalendar-config.component";
import { CircleCiConfigComponent } from "../../configs/circleci/circleci-config.component";
import { Integration } from "../models/integration";
@Injectable()
export class IntegrationService {
@ -24,53 +12,53 @@ export class IntegrationService {
"bot": {}, // empty == supported
"complex-bot": {
"rss": {
component: RssConfigComponent,
//component: RssConfigComponent,
},
"travisci": {
component: TravisCiConfigComponent,
//component: TravisCiConfigComponent,
},
"circleci": {
component: CircleCiConfigComponent,
//component: CircleCiConfigComponent,
},
},
"bridge": {
"irc": {
component: IrcConfigComponent,
//component: IrcConfigComponent,
},
},
"widget": {
"customwidget": {
component: CustomWidgetConfigComponent,
//component: CustomWidgetConfigComponent,
types: WIDGET_CUSTOM,
},
"youtube": {
component: YoutubeWidgetConfigComponent,
//component: YoutubeWidgetConfigComponent,
types: WIDGET_YOUTUBE
},
"etherpad": {
component: EtherpadWidgetConfigComponent,
//component: EtherpadWidgetConfigComponent,
types: WIDGET_ETHERPAD,
},
"twitch": {
component: TwitchWidgetConfigComponent,
//component: TwitchWidgetConfigComponent,
types: WIDGET_TWITCH,
},
"jitsi": {
component: JitsiWidgetConfigComponent,
//component: JitsiWidgetConfigComponent,
types: WIDGET_JITSI,
},
"googledocs": {
component: GoogleDocsWidgetConfigComponent,
//component: GoogleDocsWidgetConfigComponent,
types: WIDGET_GOOGLE_DOCS,
},
"googlecalendar": {
component: GoogleCalendarWidgetConfigComponent,
//component: GoogleCalendarWidgetConfigComponent,
types: WIDGET_GOOGLE_CALENDAR,
},
},
};
static getAllConfigComponents(): ContainerContent[] {
static getAllConfigComponents(): Component[] {
const components = [];
for (const iType of Object.keys(IntegrationService.supportedIntegrationsMap)) {
@ -84,34 +72,30 @@ export class IntegrationService {
}
static isSupported(integration: Integration): boolean {
const forType = IntegrationService.supportedIntegrationsMap[integration.type];
const forType = IntegrationService.supportedIntegrationsMap[integration.category];
if (!forType) return false;
if (Object.keys(forType).length === 0) return true;
return forType[integration.integrationType]; // has sub type
return forType[integration.type]; // has sub type
}
static hasConfig(integration: Integration): boolean {
return integration.type !== "bot";
static getConfigComponent(integration: Integration): Component {
return IntegrationService.supportedIntegrationsMap[integration.category][integration.type].component;
}
static getConfigComponent(integration: Integration): ContainerContent {
return IntegrationService.supportedIntegrationsMap[integration.type][integration.integrationType].component;
}
static getIntegrationForScreen(screen: string): { type: string, integrationType: string } {
static getIntegrationForScreen(screen: string): { category: string, type: string } {
for (const iType of Object.keys(IntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(IntegrationService.supportedIntegrationsMap[iType])) {
const integrationTypes = IntegrationService.supportedIntegrationsMap[iType][iiType].types;
const integrationScreens = integrationTypes.map(t => "type_" + t);
if (integrationScreens.includes(screen)) return {type: iType, integrationType: iiType};
if (integrationScreens.includes(screen)) return {category: iType, type: iiType};
}
}
return null;
}
constructor() {
private constructor() {
}
}

View File

@ -1,6 +1,6 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { Integration } from "../models/integration";
import { LegacyIntegration } from "../../models/legacyintegration";
@Injectable()
export class ApiService {
@ -17,7 +17,7 @@ export class ApiService {
.map(res => res.status === 200 ? res.json()["userId"] : null).toPromise();
}
getIntegrations(roomId: string, scalarToken: string): Promise<Integration[]> {
getIntegrations(roomId: string, scalarToken: string): Promise<LegacyIntegration[]> {
return this.http.get("/api/v1/dimension/integrations/" + roomId, {params: {scalar_token: scalarToken}})
.map(res => res.json()).toPromise();
}
@ -46,7 +46,7 @@ export class ApiService {
.map(res => res.json()).toPromise();
}
getIntegration(type: string, integrationType: string): Promise<Integration> {
getIntegration(type: string, integrationType: string): Promise<LegacyIntegration> {
const url = "/api/v1/dimension/integration/" + type + "/" + integrationType;
return this.http.get(url).map(res => res.json()).toPromise();
}

View File

@ -1,24 +1,24 @@
import { Injectable } from "@angular/core";
import { Integration } from "./models/integration";
import { RssConfigComponent } from "../configs/rss/rss-config.component";
import { LegacyIntegration } from "../../models/legacyintegration";
import { RssConfigComponent } from "../../../configs/rss/rss-config.component";
import { ContainerContent } from "ngx-modialog";
import { IrcConfigComponent } from "../configs/irc/irc-config.component";
import { TravisCiConfigComponent } from "../configs/travisci/travisci-config.component";
import { CircleCiConfigComponent } from "../configs/circleci/circleci-config.component";
import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/custom_widget-config.component";
import { YoutubeWidgetConfigComponent } from "../configs/widget/youtube/youtube-config.component";
import { TwitchWidgetConfigComponent } from "../configs/widget/twitch/twitch-config.component";
import { EtherpadWidgetConfigComponent } from "../configs/widget/etherpad/etherpad-config.component";
import { JitsiWidgetConfigComponent } from "../configs/widget/jitsi/jitsi-config.component";
import { IrcConfigComponent } from "../../../configs/irc/irc-config.component";
import { TravisCiConfigComponent } from "../../../configs/travisci/travisci-config.component";
import { CustomWidgetConfigComponent } from "../../../configs/widget/custom_widget/custom_widget-config.component";
import { YoutubeWidgetConfigComponent } from "../../../configs/widget/youtube/youtube-config.component";
import { TwitchWidgetConfigComponent } from "../../../configs/widget/twitch/twitch-config.component";
import { EtherpadWidgetConfigComponent } from "../../../configs/widget/etherpad/etherpad-config.component";
import { JitsiWidgetConfigComponent } from "../../../configs/widget/jitsi/jitsi-config.component";
import {
WIDGET_CUSTOM, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, WIDGET_JITSI, WIDGET_TWITCH,
WIDGET_YOUTUBE
} from "./models/widget";
import { GoogleDocsWidgetConfigComponent } from "../configs/widget/googledocs/googledocs-config.component";
import { GoogleCalendarWidgetConfigComponent } from "../configs/widget/googlecalendar/googlecalendar-config.component";
} from "../../models/widget";
import { GoogleDocsWidgetConfigComponent } from "../../../configs/widget/googledocs/googledocs-config.component";
import { GoogleCalendarWidgetConfigComponent } from "../../../configs/widget/googlecalendar/googlecalendar-config.component";
import { CircleCiConfigComponent } from "../../../configs/circleci/circleci-config.component";
@Injectable()
export class IntegrationService {
export class LegacyIntegrationService {
private static supportedIntegrationsMap = {
"bot": {}, // empty == supported
@ -73,9 +73,9 @@ export class IntegrationService {
static getAllConfigComponents(): ContainerContent[] {
const components = [];
for (const iType of Object.keys(IntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(IntegrationService.supportedIntegrationsMap[iType])) {
const component = IntegrationService.supportedIntegrationsMap[iType][iiType].component;
for (const iType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap[iType])) {
const component = LegacyIntegrationService.supportedIntegrationsMap[iType][iiType].component;
if (component) components.push(component);
}
}
@ -83,8 +83,8 @@ export class IntegrationService {
return components;
}
static isSupported(integration: Integration): boolean {
const forType = IntegrationService.supportedIntegrationsMap[integration.type];
static isSupported(integration: LegacyIntegration): boolean {
const forType = LegacyIntegrationService.supportedIntegrationsMap[integration.type];
if (!forType) return false;
if (Object.keys(forType).length === 0) return true;
@ -92,18 +92,18 @@ export class IntegrationService {
return forType[integration.integrationType]; // has sub type
}
static hasConfig(integration: Integration): boolean {
static hasConfig(integration: LegacyIntegration): boolean {
return integration.type !== "bot";
}
static getConfigComponent(integration: Integration): ContainerContent {
return IntegrationService.supportedIntegrationsMap[integration.type][integration.integrationType].component;
static getConfigComponent(integration: LegacyIntegration): ContainerContent {
return LegacyIntegrationService.supportedIntegrationsMap[integration.type][integration.integrationType].component;
}
static getIntegrationForScreen(screen: string): { type: string, integrationType: string } {
for (const iType of Object.keys(IntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(IntegrationService.supportedIntegrationsMap[iType])) {
const integrationTypes = IntegrationService.supportedIntegrationsMap[iType][iiType].types;
for (const iType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap)) {
for (const iiType of Object.keys(LegacyIntegrationService.supportedIntegrationsMap[iType])) {
const integrationTypes = LegacyIntegrationService.supportedIntegrationsMap[iType][iiType].types;
const integrationScreens = integrationTypes.map(t => "type_" + t);
if (integrationScreens.includes(screen)) return {type: iType, integrationType: iiType};
}

View File

@ -6,17 +6,17 @@ import {
MembershipStateResponse, RoomEncryptionStatusResponse,
ScalarSuccessResponse,
WidgetsResponse
} from "../models/scalar_responses";
} from "../models/scalar_client_responses";
import { EditableWidget } from "../models/widget";
@Injectable()
export class ScalarService {
export class ScalarClientApiService {
private static actionMap: { [key: string]: { resolve: (obj: any) => void, reject: (obj: any) => void } } = {};
public static getAndRemoveActionHandler(requestKey: string): { resolve: (obj: any) => void, reject: (obj: any) => void } {
let handler = ScalarService.actionMap[requestKey];
ScalarService.actionMap[requestKey] = null;
let handler = ScalarClientApiService.actionMap[requestKey];
ScalarClientApiService.actionMap[requestKey] = null;
return handler;
}
@ -96,7 +96,7 @@ export class ScalarService {
return;
}
ScalarService.actionMap[requestKey] = {
ScalarClientApiService.actionMap[requestKey] = {
resolve: resolve,
reject: reject
};
@ -117,7 +117,7 @@ window.addEventListener("message", event => {
let requestKey = event.data["request_id"];
if (!requestKey) return;
let action = ScalarService.getAndRemoveActionHandler(requestKey);
let action = ScalarClientApiService.getAndRemoveActionHandler(requestKey);
if (!action) return;
if (event.data.response && event.data.response.error) action.reject(event.data);

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { ScalarAccountResponse } from "../models/scalar_server_responses";
import { AuthedApi } from "./AuthedApi";
@Injectable()
export class ScalarServerApiService extends AuthedApi {
constructor(http: Http) {
super(http)
}
public getAccount(): Promise<ScalarAccountResponse> {
return this.authedGet("/api/v1/scalar/account").map(res => res.json()).toPromise();
}
}

View File

@ -1,5 +1,5 @@
import { Component } from "@angular/core";
import { ApiService } from "../../shared/services/api.service";
import { ApiService } from "../../shared/services/legacy/api.service";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";

View File

@ -1,8 +1,8 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import * as $ from "jquery";
import { ApiService } from "../../shared/services/api.service";
import { JitsiWidgetIntegration } from "../../shared/models/integration";
import { ApiService } from "../../shared/services/legacy/api.service";
import { JitsiWidgetIntegration } from "../../shared/models/legacyintegration";
declare var JitsiMeetExternalAPI: any;