mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-07-01 00:31:23 +00:00
Add the start of an admin API and re-add widgets
The frontend is still broken and doesn't use these endpoints at all. A migration tool still needs to be written to pull in existing widget configurations.
This commit is contained in:
parent
826364e803
commit
599fb80112
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -10,6 +10,10 @@ config/integrations/*_development.yaml
|
|||
config/integrations/*_production.yaml
|
||||
build/
|
||||
dimension.db
|
||||
src-ts/**/*.js
|
||||
src-ts/**/*.js.map
|
||||
web/**/*.js
|
||||
web/**/*.js.map
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -5478,8 +5478,7 @@
|
|||
"path-parse": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
|
||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
||||
"dev": true
|
||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
|
@ -7524,7 +7523,6 @@
|
|||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
|
||||
"integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-parse": "1.0.5"
|
||||
}
|
||||
|
@ -9583,6 +9581,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"umzug": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.1.0.tgz",
|
||||
"integrity": "sha512-BgT+ekpItEWaG+3JjLLj6yVTxw2wIH8Cr6JyKYIzukWAx9nzGhC6BGHb/IRMjpobMM1qtIrReATwLUjKpU2iOQ==",
|
||||
"requires": {
|
||||
"babel-runtime": "6.26.0",
|
||||
"bluebird": "3.5.1",
|
||||
"lodash": "4.17.4",
|
||||
"resolve": "1.4.0"
|
||||
}
|
||||
},
|
||||
"union-value": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
|
||||
|
|
|
@ -40,11 +40,10 @@
|
|||
"sequelize-typescript": "^0.6.1",
|
||||
"sqlite3": "^3.1.13",
|
||||
"typescript-rest": "^1.2.0",
|
||||
"umzug": "^2.1.0",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"embed-video": "^2.0.0",
|
||||
"screenfull": "^3.3.2",
|
||||
"@angular/animations": "^5.0.0",
|
||||
"@angular/common": "^5.0.0",
|
||||
"@angular/compiler": "^5.0.0",
|
||||
|
@ -67,6 +66,7 @@
|
|||
"core-js": "^2.5.2",
|
||||
"css-loader": "^0.28.7",
|
||||
"cssnano": "^3.10.0",
|
||||
"embed-video": "^2.0.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"goby": "^1.1.2",
|
||||
|
@ -86,13 +86,13 @@
|
|||
"rimraf": "^2.6.2",
|
||||
"rxjs": "^5.5.5",
|
||||
"sass-loader": "^6.0.3",
|
||||
"screenfull": "^3.3.2",
|
||||
"shelljs": "^0.7.8",
|
||||
"spinkit": "^1.2.5",
|
||||
"style-loader": "^0.19.1",
|
||||
"ts-helpers": "^1.1.2",
|
||||
"tslint": "^5.8.0",
|
||||
"tslint-loader": "^3.4.3",
|
||||
"typescript": "^2.6.2",
|
||||
"url-loader": "^0.6.2",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-dev-server": "^2.9.7",
|
||||
|
|
|
@ -22,7 +22,7 @@ export default class Webserver {
|
|||
private loadRoutes() {
|
||||
const apis = ["scalar", "dimension"].map(a => path.join(__dirname, a, "*.js"));
|
||||
const router = express.Router();
|
||||
Server.loadServices(router, apis);
|
||||
apis.forEach(a => Server.loadServices(router, [a]));
|
||||
const routes = _.uniq(router.stack.map(r => r.route.path));
|
||||
for (const route of routes) {
|
||||
this.app.options(route, (_req, res) => res.sendStatus(200));
|
||||
|
|
35
src-ts/api/dimension/DimensionAdminService.ts
Normal file
35
src-ts/api/dimension/DimensionAdminService.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { GET, Path, QueryParam } from "typescript-rest";
|
||||
import * as Promise from "bluebird";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import config from "../../config";
|
||||
import { ApiError } from "../ApiError";
|
||||
|
||||
interface DimensionInfoResponse {
|
||||
admins: string[],
|
||||
}
|
||||
|
||||
@Path("/api/v1/dimension/admin")
|
||||
export class DimensionAdminService {
|
||||
|
||||
public static isAdmin(userId: string) {
|
||||
return config.admins.indexOf(userId) >= 0;
|
||||
}
|
||||
|
||||
public static validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
|
||||
return ScalarService.getTokenOwner(scalarToken).then(userId => {
|
||||
if (!DimensionAdminService.isAdmin(userId))
|
||||
throw new ApiError(401, {message: "You must be an administrator to use this API"});
|
||||
else return userId;
|
||||
}, ScalarService.invalidTokenErrorHandler);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("info")
|
||||
public getInfo(@QueryParam("scalar_token") scalarToken: string): Promise<DimensionInfoResponse> {
|
||||
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
|
||||
// Only let admins see other admins
|
||||
// A 200 OK essentially means "you're an admin".
|
||||
return {admins: config.admins};
|
||||
});
|
||||
}
|
||||
}
|
54
src-ts/api/dimension/DimensionIntegrationsService.ts
Normal file
54
src-ts/api/dimension/DimensionIntegrationsService.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { GET, Path, QueryParam } from "typescript-rest";
|
||||
import * as Promise from "bluebird";
|
||||
import { ScalarService } from "../scalar/ScalarService";
|
||||
import { DimensionStore } from "../../db/DimensionStore";
|
||||
import { DimensionAdminService } from "./DimensionAdminService";
|
||||
import { Widget } from "../../integrations/Widget";
|
||||
import { MemoryCache } from "../../MemoryCache";
|
||||
|
||||
interface IntegrationsResponse {
|
||||
widgets: Widget[],
|
||||
}
|
||||
|
||||
@Path("/api/v1/dimension/integrations")
|
||||
export class DimensionIntegrationsService {
|
||||
|
||||
private static integrationCache = new MemoryCache();
|
||||
|
||||
public static clearIntegrationCache() {
|
||||
DimensionIntegrationsService.integrationCache.clear();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("enabled")
|
||||
public getEnabledIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise<IntegrationsResponse> {
|
||||
return ScalarService.getTokenOwner(scalarToken).then(_userId => {
|
||||
return this.getIntegrations(true);
|
||||
}, ScalarService.invalidTokenErrorHandler);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public getAllIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise<IntegrationsResponse> {
|
||||
return DimensionAdminService.validateAndGetAdminTokenOwner(scalarToken).then(_userId => {
|
||||
return this.getIntegrations(null);
|
||||
});
|
||||
}
|
||||
|
||||
private getIntegrations(isEnabledCheck?: boolean): Promise<IntegrationsResponse> {
|
||||
const cachedResponse = DimensionIntegrationsService.integrationCache.get("integrations_" + isEnabledCheck);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
const response = <IntegrationsResponse>{
|
||||
widgets: [],
|
||||
};
|
||||
return Promise.resolve()
|
||||
.then(() => DimensionStore.getWidgets(isEnabledCheck))
|
||||
.then(widgets => response.widgets = widgets)
|
||||
|
||||
// Cache and return response
|
||||
.then(() => DimensionIntegrationsService.integrationCache.put("integrations_" + isEnabledCheck, response))
|
||||
.then(() => response);
|
||||
}
|
||||
}
|
|
@ -40,6 +40,14 @@ export class ScalarService {
|
|||
});
|
||||
}
|
||||
|
||||
public static invalidTokenErrorHandler(error: any): any {
|
||||
if (error !== "Invalid token") {
|
||||
LogService.error("ScalarWidgetService", "Error processing request");
|
||||
LogService.error("ScalarWidgetService", error);
|
||||
}
|
||||
throw new ApiError(401, {message: "Invalid token"});
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("register")
|
||||
public register(request: RegisterRequest): Promise<ScalarRegisterResponse> {
|
||||
|
@ -94,13 +102,7 @@ export class ScalarService {
|
|||
public getAccount(@QueryParam("scalar_token") scalarToken: string): Promise<ScalarAccountResponse> {
|
||||
return ScalarService.getTokenOwner(scalarToken).then(userId => {
|
||||
return {user_id: userId};
|
||||
}, err => {
|
||||
if (err !== "Invalid token") {
|
||||
LogService.error("ScalarService", "Error getting account information for user");
|
||||
LogService.error("ScalarService", err);
|
||||
}
|
||||
throw new ApiError(401, {message: "Invalid token"});
|
||||
});
|
||||
}, ScalarService.invalidTokenErrorHandler);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { GET, Path, QueryParam } from "typescript-rest";
|
||||
import * as Promise from "bluebird";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { MemoryCache } from "../../MemoryCache";
|
||||
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
||||
import config from "../../config";
|
||||
|
@ -67,15 +66,9 @@ export class ScalarWidgetService {
|
|||
|
||||
@GET
|
||||
@Path("title_lookup")
|
||||
public register(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
||||
public titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
||||
return ScalarService.getTokenOwner(scalarToken).then(_userId => {
|
||||
return ScalarWidgetService.getUrlTitle(url);
|
||||
}, err => {
|
||||
if (err !== "Invalid token") {
|
||||
LogService.error("ScalarWidgetService", "Error getting account information for user");
|
||||
LogService.error("ScalarWidgetService", err);
|
||||
}
|
||||
throw new ApiError(401, {message: "Invalid token"});
|
||||
})
|
||||
}, ScalarService.invalidTokenErrorHandler);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,10 @@ import User from "./models/User";
|
|||
import UserScalarToken from "./models/UserScalarToken";
|
||||
import Upstream from "./models/Upstream";
|
||||
import * as Promise from "bluebird";
|
||||
import WidgetRecord from "./models/WidgetRecord";
|
||||
import * as path from "path";
|
||||
import * as Umzug from "umzug";
|
||||
import { Widget } from "../integrations/Widget";
|
||||
|
||||
class _DimensionStore {
|
||||
private sequelize: Sequelize;
|
||||
|
@ -22,12 +26,23 @@ class _DimensionStore {
|
|||
User,
|
||||
UserScalarToken,
|
||||
Upstream,
|
||||
WidgetRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
public updateSchema(): Promise<any> {
|
||||
LogService.info("DimensionStore", "Updating schema...");
|
||||
return this.sequelize.sync();
|
||||
|
||||
const migrator = new Umzug({
|
||||
storage: "sequelize",
|
||||
storageOptions: {sequelize: this.sequelize},
|
||||
migrations: {
|
||||
params: [this.sequelize.getQueryInterface()],
|
||||
path: path.join(__dirname, "migrations"),
|
||||
}
|
||||
});
|
||||
|
||||
return migrator.up();
|
||||
}
|
||||
|
||||
public doesUserHaveTokensForAllUpstreams(userId: string): Promise<boolean> {
|
||||
|
@ -56,7 +71,10 @@ class _DimensionStore {
|
|||
|
||||
public getTokenOwner(scalarToken: string): Promise<User> {
|
||||
let user: User = null;
|
||||
return UserScalarToken.findAll({where: {isDimensionToken: true, scalarToken: scalarToken}, include: [User]}).then(tokens => {
|
||||
return UserScalarToken.findAll({
|
||||
where: {isDimensionToken: true, scalarToken: scalarToken},
|
||||
include: [User]
|
||||
}).then(tokens => {
|
||||
if (!tokens || tokens.length === 0) {
|
||||
return Promise.reject("Invalid token");
|
||||
}
|
||||
|
@ -70,6 +88,12 @@ class _DimensionStore {
|
|||
return Promise.resolve(user);
|
||||
});
|
||||
}
|
||||
|
||||
public getWidgets(isEnabled?: boolean): Promise<Widget[]> {
|
||||
let conditions = {};
|
||||
if (isEnabled === true || isEnabled === false) conditions = {where: {isEnabled: isEnabled}};
|
||||
return WidgetRecord.findAll(conditions).then(widgets => widgets.map(w => new Widget(w)));
|
||||
}
|
||||
}
|
||||
|
||||
export const DimensionStore = new _DimensionStore();
|
42
src-ts/db/migrations/20171218203245-AddTables.ts
Normal file
42
src-ts/db/migrations/20171218203245-AddTables.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
import * as Promise from "bluebird";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_users", {
|
||||
"userId": {type: DataType.STRING, primaryKey: true, allowNull: false},
|
||||
}))
|
||||
.then(() => queryInterface.createTable("dimension_upstreams", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"name": {type: DataType.STRING, allowNull: false},
|
||||
"type": {type: DataType.STRING, allowNull: false},
|
||||
"scalarUrl": {type: DataType.STRING, allowNull: false},
|
||||
"apiUrl": {type: DataType.STRING, allowNull: false},
|
||||
}))
|
||||
.then(() => queryInterface.createTable("dimension_scalar_tokens", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"userId": {
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
references: {model: "dimension_users", key: "userId"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
"scalarToken": {type: DataType.STRING, allowNull: false},
|
||||
"isDimensionToken": {type: DataType.BOOLEAN, allowNull: false},
|
||||
"upstreamId": {
|
||||
type: DataType.INTEGER,
|
||||
allowNull: true,
|
||||
references: {model: "dimension_upstreams", key: "id"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
}));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.dropTable("dimension_users"))
|
||||
.then(() => queryInterface.dropTable("dimension_upstreams"))
|
||||
.then(() => queryInterface.dropTable("dimension_scalar_tokens"));
|
||||
}
|
||||
}
|
72
src-ts/db/migrations/20171218203245-AddWidgets.ts
Normal file
72
src-ts/db/migrations/20171218203245-AddWidgets.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
import * as Promise from "bluebird";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_widgets", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"type": {type: DataType.STRING, allowNull: false},
|
||||
"name": {type: DataType.STRING, allowNull: false},
|
||||
"avatarUrl": {type: DataType.STRING, allowNull: false},
|
||||
"description": {type: DataType.STRING, allowNull: false},
|
||||
"isEnabled": {type: DataType.BOOLEAN, allowNull: false},
|
||||
"optionsJson": {type: DataType.STRING, allowNull: true},
|
||||
}))
|
||||
.then(() => queryInterface.bulkInsert("dimension_widgets", [
|
||||
{
|
||||
type: "custom",
|
||||
name: "Custom Widget",
|
||||
avatarUrl: "/img/avatars/customwidget.png",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
description: "A webpage embedded in the room.",
|
||||
},
|
||||
{
|
||||
type: "etherpad",
|
||||
name: "Etherpad",
|
||||
avatarUrl: "/img/avatars/etherpad.png",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
description: "Collaborate on documents with members of your room.",
|
||||
optionsJson: '{"defaultUrl":"https://demo.riot.im/etherpad/p/$roomId_$padName"}',
|
||||
},
|
||||
{
|
||||
type: "googlecalendar",
|
||||
name: "Google Calendar",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
avatarUrl: "/img/avatars/googlecalendar.png",
|
||||
description: "Share upcoming events in your room with a Google Calendar.",
|
||||
},
|
||||
{
|
||||
type: "googledocs",
|
||||
name: "Google Docs",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
avatarUrl: "/img/avatars/googledocs.png",
|
||||
description: "Collaborate on and share documents using Google Docs.",
|
||||
},
|
||||
{
|
||||
type: "youtube",
|
||||
name: "YouTube Video",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
avatarUrl: "/img/avatars/youtube.png",
|
||||
description: "Embed a YouTube, Vimeo, or DailyMotion video in your room.",
|
||||
},
|
||||
{
|
||||
type: "twitch",
|
||||
name: "Twitch Livestream",
|
||||
isEnabled: true,
|
||||
isPublic: true,
|
||||
avatarUrl: "/img/avatars/twitch.png",
|
||||
description: "Embed a Twitch livestream into your room.",
|
||||
},
|
||||
]));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return queryInterface.dropTable("dimension_widgets");
|
||||
}
|
||||
}
|
8
src-ts/db/models/IntegrationRecord.ts
Normal file
8
src-ts/db/models/IntegrationRecord.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface IntegrationRecord {
|
||||
type: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
description: string;
|
||||
isEnabled: boolean;
|
||||
isPublic: boolean;
|
||||
}
|
35
src-ts/db/models/WidgetRecord.ts
Normal file
35
src-ts/db/models/WidgetRecord.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||
import { IntegrationRecord } from "./IntegrationRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_widgets",
|
||||
underscoredAll: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class WidgetRecord extends Model<WidgetRecord> implements IntegrationRecord {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
type: string;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@Column
|
||||
avatarUrl: string;
|
||||
|
||||
@Column
|
||||
description: string;
|
||||
|
||||
@Column
|
||||
isEnabled: boolean;
|
||||
|
||||
@Column
|
||||
isPublic: boolean;
|
||||
|
||||
@Column
|
||||
optionsJson: string;
|
||||
}
|
30
src-ts/integrations/Integration.ts
Normal file
30
src-ts/integrations/Integration.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { IntegrationRecord } from "../db/models/IntegrationRecord";
|
||||
|
||||
export class Integration {
|
||||
// These are meant to be set by the underlying integration
|
||||
public category: "bot" | "complex-bot" | "bridge" | "widget";
|
||||
public type: string;
|
||||
public requirements: IntegrationRequirement[];
|
||||
|
||||
// These are meant to be set by us
|
||||
public displayName: string;
|
||||
public avatarUrl: string;
|
||||
public description: string;
|
||||
public isEnabled: boolean;
|
||||
public isPublic: boolean;
|
||||
|
||||
constructor(record: IntegrationRecord) {
|
||||
this.type = record.type;
|
||||
this.displayName = record.name;
|
||||
this.avatarUrl = record.avatarUrl;
|
||||
this.description = record.description;
|
||||
this.isEnabled = record.isEnabled;
|
||||
this.isPublic = record.isPublic;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IntegrationRequirement {
|
||||
condition: "publicRoom" | "canSendEventTypes";
|
||||
argument: any;
|
||||
expectedValue: any;
|
||||
}
|
21
src-ts/integrations/Widget.ts
Normal file
21
src-ts/integrations/Widget.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Integration } from "./Integration";
|
||||
import WidgetRecord from "../db/models/WidgetRecord";
|
||||
|
||||
export interface EtherpadWidgetOptions {
|
||||
defaultUrl: string;
|
||||
}
|
||||
|
||||
export class Widget extends Integration {
|
||||
public options: any;
|
||||
|
||||
constructor(widgetRecord: WidgetRecord) {
|
||||
super(widgetRecord);
|
||||
this.category = "widget";
|
||||
this.options = widgetRecord.optionsJson ? JSON.parse(widgetRecord.optionsJson) : {};
|
||||
this.requirements = [{
|
||||
condition: "canSendEventTypes",
|
||||
argument: ["im.vector.widget"],
|
||||
expectedValue: true,
|
||||
}];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user