mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-10-01 05:05:53 +00:00
Merge branch 'travis/msc2140-tos'
This commit is contained in:
commit
dd53cb8484
30
package-lock.json
generated
30
package-lock.json
generated
@ -49,15 +49,6 @@
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@angular/http": {
|
||||
"version": "7.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@angular/http/-/http-7.2.15.tgz",
|
||||
"integrity": "sha512-TR7PEdmLWNIre3Zn8lvyb4lSrvPUJhKLystLnp4hBMcWsJqq5iK8S3bnlR4viZ9HMlf7bW7+Hm4SI6aB3tdUtw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@angular/platform-browser": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.0.3.tgz",
|
||||
@ -921,6 +912,21 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@ckeditor/ckeditor5-angular": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-angular/-/ckeditor5-angular-1.1.0.tgz",
|
||||
"integrity": "sha512-6b9NX/PhFuQKo/mR0tSpCVNGU1fhg1Y8ju5OBWxCPpIbdDZIIoqjHlhWL76EmfztteouYLACsnfrnqVQGC0utA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"@ckeditor/ckeditor5-build-classic": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-build-classic/-/ckeditor5-build-classic-12.2.0.tgz",
|
||||
"integrity": "sha512-En64jC5ImZoa+XLa2JrZYCKpq2iNXhdf17xgmpJoAXp+m36EqSHd6/4x0XT/pjrZSDxrcLUZxyZJlQCRtSaeHw==",
|
||||
"dev": true
|
||||
},
|
||||
"@fortawesome/fontawesome": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome/-/fontawesome-1.1.8.tgz",
|
||||
@ -6148,6 +6154,12 @@
|
||||
"integrity": "sha1-ojR7o2DeGeM9D/1ZD933dVy/LmQ=",
|
||||
"dev": true
|
||||
},
|
||||
"iso-639-1": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.0.5.tgz",
|
||||
"integrity": "sha512-2TcJ8AcsqM4AXLi92eFZX3xa7X6Eno/chq9yOR0AvSgb15Smmoh1miXyYJVWCkSmbzDimds3Ix2M4efhnOuxOg==",
|
||||
"dev": true
|
||||
},
|
||||
"isobject": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
|
@ -63,7 +63,6 @@
|
||||
"@angular/compiler": "^8.0.3",
|
||||
"@angular/core": "^8.0.3",
|
||||
"@angular/forms": "^8.0.3",
|
||||
"@angular/http": "^7.2.15",
|
||||
"@angular/platform-browser": "^8.0.3",
|
||||
"@angular/platform-browser-dynamic": "^8.0.3",
|
||||
"@angular/router": "^8.0.3",
|
||||
@ -71,6 +70,8 @@
|
||||
"@angularclass/hmr-loader": "^3.0.4",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@ckeditor/ckeditor5-angular": "^1.1.0",
|
||||
"@ckeditor/ckeditor5-build-classic": "^12.2.0",
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-brands": "^5.0.13",
|
||||
"@fortawesome/fontawesome-free-regular": "^5.0.13",
|
||||
@ -93,6 +94,7 @@
|
||||
"goby": "^1.1.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"iso-639-1": "^2.0.5",
|
||||
"jquery": "^3.4.1",
|
||||
"json-loader": "^0.5.7",
|
||||
"mini-css-extract-plugin": "^0.7.0",
|
||||
|
@ -54,4 +54,5 @@ export const CACHE_TELEGRAM_BRIDGE = "telegram-bridge";
|
||||
export const CACHE_WEBHOOKS_BRIDGE = "webhooks-bridge";
|
||||
export const CACHE_GITTER_BRIDGE = "gitter-bridge";
|
||||
export const CACHE_SIMPLE_BOTS = "simple-bots";
|
||||
export const CACHE_SLACK_BRIDGE = "slack-bridge";
|
||||
export const CACHE_SLACK_BRIDGE = "slack-bridge";
|
||||
export const CACHE_TERMS = "terms";
|
@ -26,16 +26,19 @@ export class ApiError {
|
||||
* then converted to JSON as {message: "your_message"})
|
||||
* @param {string} errCode The internal error code to describe what went wrong
|
||||
*/
|
||||
constructor(statusCode: number, json: string | object, errCode = "D_UNKNOWN") {
|
||||
constructor(statusCode: number, json: string | object, errCode = "M_UNKNOWN") {
|
||||
// Because typescript is just plain dumb
|
||||
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript
|
||||
Error.apply(this, ["ApiError"]);
|
||||
|
||||
if (typeof(json) === "string") json = {message: json};
|
||||
if (typeof (json) === "string") json = {message: json};
|
||||
this.jsonResponse = json;
|
||||
this.statusCode = statusCode;
|
||||
this.errorCode = errCode;
|
||||
|
||||
this.jsonResponse["dim_errcode"] = this.errorCode;
|
||||
|
||||
if (!this.jsonResponse['error']) this.jsonResponse['error'] = this.jsonResponse['message'];
|
||||
if (!this.jsonResponse['errcode']) this.jsonResponse['errcode'] = errCode;
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import { Server } from "typescript-rest";
|
||||
import * as _ from "lodash";
|
||||
import config from "../config";
|
||||
import { ApiError } from "./ApiError";
|
||||
import MSCSecurity from "./security/MSCSecurity";
|
||||
import MatrixSecurity from "./security/MatrixSecurity";
|
||||
|
||||
/**
|
||||
* Web server for Dimension. Handles the API routes for the admin, scalar, dimension, and matrix APIs.
|
||||
@ -26,10 +26,10 @@ export default class Webserver {
|
||||
private loadRoutes() {
|
||||
// TODO: Rename services to controllers, and controllers to services. They're backwards.
|
||||
|
||||
const apis = ["scalar", "dimension", "admin", "matrix", "msc"].map(a => path.join(__dirname, a, "*.js"));
|
||||
const apis = ["scalar", "dimension", "admin", "matrix"].map(a => path.join(__dirname, a, "*.js"));
|
||||
const router = express.Router();
|
||||
Server.useIoC();
|
||||
Server.registerAuthenticator(new MSCSecurity());
|
||||
Server.registerAuthenticator(new MatrixSecurity());
|
||||
apis.forEach(a => Server.loadServices(router, [a]));
|
||||
const routes = _.uniq(router.stack.map(r => r.route.path));
|
||||
for (const route of routes) {
|
||||
@ -71,8 +71,13 @@ export default class Webserver {
|
||||
next();
|
||||
});
|
||||
this.app.use((_req, res, next) => {
|
||||
if (res.headersSent) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import AppService from "../../db/models/AppService";
|
||||
import { AppserviceStore } from "../../db/AppserviceStore";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { MatrixAppserviceClient } from "../../matrix/MatrixAppserviceClient";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface AppserviceResponse {
|
||||
id: string;
|
||||
@ -23,18 +23,20 @@ interface AppserviceCreateRequest {
|
||||
@Path("/api/v1/dimension/admin/appservices")
|
||||
export class AdminAppserviceService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getAppservices(@QueryParam("scalar_token") scalarToken: string): Promise<AppserviceResponse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getAppservices(): Promise<AppserviceResponse[]> {
|
||||
return (await AppService.findAll()).map(a => this.mapAppservice(a));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(":appserviceId")
|
||||
public async getAppservice(@QueryParam("scalar_token") scalarToken: string, @PathParam("appserviceId") asId: string): Promise<AppserviceResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getAppservice(@PathParam("appserviceId") asId: string): Promise<AppserviceResponse> {
|
||||
try {
|
||||
const appservice = await AppserviceStore.getAppservice(asId);
|
||||
return this.mapAppservice(appservice);
|
||||
@ -46,8 +48,9 @@ export class AdminAppserviceService {
|
||||
|
||||
@POST
|
||||
@Path("new")
|
||||
public async createAppservice(@QueryParam("scalar_token") scalarToken: string, request: AppserviceCreateRequest): Promise<AppserviceResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async createAppservice(request: AppserviceCreateRequest): Promise<AppserviceResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
// Trim off the @ sign if it's on the prefix
|
||||
if (request.userPrefix[0] === "@") {
|
||||
@ -66,9 +69,8 @@ export class AdminAppserviceService {
|
||||
|
||||
@POST
|
||||
@Path(":appserviceId/test")
|
||||
public async test(@QueryParam("scalar_token") scalarToken: string, @PathParam("appserviceId") asId: string): Promise<any> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async test(@PathParam("appserviceId") asId: string): Promise<any> {
|
||||
const appservice = await AppserviceStore.getAppservice(asId);
|
||||
const client = new MatrixAppserviceClient(appservice);
|
||||
const userId = await client.whoAmI();
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { BotStore } from "../../db/BotStore";
|
||||
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface BotResponse extends BotRequest {
|
||||
id: number;
|
||||
@ -31,18 +31,20 @@ interface BotProfile {
|
||||
@Path("/api/v1/dimension/admin/bots/simple/custom")
|
||||
export class AdminCustomSimpleBotService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getBots(@QueryParam("scalar_token") scalarToken: string): Promise<BotResponse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBots(): Promise<BotResponse[]> {
|
||||
return BotStore.getCustomBots();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(":botId")
|
||||
public async getBot(@QueryParam("scalar_token") scalarToken: string, @PathParam("botId") botId: number): Promise<BotResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBot(@PathParam("botId") botId: number): Promise<BotResponse> {
|
||||
const bot = await BotStore.getCustomBot(botId);
|
||||
if (!bot) throw new ApiError(404, "Bot not found");
|
||||
return bot;
|
||||
@ -50,9 +52,9 @@ export class AdminCustomSimpleBotService {
|
||||
|
||||
@POST
|
||||
@Path("new")
|
||||
public async createBot(@QueryParam("scalar_token") scalarToken: string, request: BotRequest): Promise<BotResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async createBot(request: BotRequest): Promise<BotResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const bot = await BotStore.createCustom(request);
|
||||
LogService.info("AdminCustomSimpleBotService", userId + " created a simple bot");
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
@ -61,9 +63,9 @@ export class AdminCustomSimpleBotService {
|
||||
|
||||
@POST
|
||||
@Path(":botId")
|
||||
public async updateBot(@QueryParam("scalar_token") scalarToken: string, @PathParam("botId") botId: number, request: BotRequest): Promise<BotResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async updateBot(@PathParam("botId") botId: number, request: BotRequest): Promise<BotResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const bot = await BotStore.updateCustom(botId, request);
|
||||
LogService.info("AdminCustomSimpleBotService", userId + " updated a simple bot");
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
@ -72,9 +74,9 @@ export class AdminCustomSimpleBotService {
|
||||
|
||||
@DELETE
|
||||
@Path(":botId")
|
||||
public async deleteBot(@QueryParam("scalar_token") scalarToken: string, @PathParam("botId") botId: number): Promise<any> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async deleteBot(@PathParam("botId") botId: number): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
await BotStore.deleteCustom(botId);
|
||||
LogService.info("AdminCustomSimpleBotService", userId + " deleted a simple bot");
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
@ -83,9 +85,8 @@ export class AdminCustomSimpleBotService {
|
||||
|
||||
@GET
|
||||
@Path("profile/:userId")
|
||||
public async getProfile(@QueryParam("scalar_token") scalarToken: string, @PathParam("userId") userId: string): Promise<BotProfile> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getProfile(@PathParam("userId") userId: string): Promise<BotProfile> {
|
||||
const profile = await BotStore.getProfile(userId);
|
||||
return {name: profile.displayName, avatarUrl: profile.avatarMxc};
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_GITTER_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import GitterBridgeRecord from "../../db/models/GitterBridgeRecord";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreateWithUpstream {
|
||||
upstreamId: number;
|
||||
@ -27,11 +27,13 @@ interface BridgeResponse {
|
||||
@Path("/api/v1/dimension/admin/gitter")
|
||||
export class AdminGitterService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridges(): Promise<BridgeResponse[]> {
|
||||
const bridges = await GitterBridgeRecord.findAll();
|
||||
return Promise.all(bridges.map(async b => {
|
||||
return {
|
||||
@ -45,9 +47,8 @@ export class AdminGitterService {
|
||||
|
||||
@GET
|
||||
@Path(":bridgeId")
|
||||
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
const telegramBridge = await GitterBridgeRecord.findByPk(bridgeId);
|
||||
if (!telegramBridge) throw new ApiError(404, "Gitter Bridge not found");
|
||||
|
||||
@ -61,9 +62,9 @@ export class AdminGitterService {
|
||||
|
||||
@POST
|
||||
@Path(":bridgeId")
|
||||
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const bridge = await GitterBridgeRecord.findByPk(bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
|
||||
@ -74,14 +75,14 @@ export class AdminGitterService {
|
||||
|
||||
Cache.for(CACHE_GITTER_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/upstream")
|
||||
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForUpstream(request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const upstream = await Upstream.findByPk(request.upstreamId);
|
||||
if (!upstream) throw new ApiError(400, "Upstream not found");
|
||||
|
||||
@ -93,14 +94,14 @@ export class AdminGitterService {
|
||||
|
||||
Cache.for(CACHE_GITTER_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/selfhosted")
|
||||
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const bridge = await GitterBridgeRecord.create({
|
||||
provisionUrl: request.provisionUrl,
|
||||
isEnabled: true,
|
||||
@ -109,6 +110,6 @@ export class AdminGitterService {
|
||||
|
||||
Cache.for(CACHE_GITTER_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { DimensionIntegrationsService } from "../dimension/DimensionIntegrationsService";
|
||||
import { WidgetStore } from "../../db/WidgetStore";
|
||||
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||
import { Integration } from "../../integrations/Integration";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { BridgeStore } from "../../db/BridgeStore";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface SetEnabledRequest {
|
||||
enabled: boolean;
|
||||
@ -23,10 +23,14 @@ interface SetOptionsRequest {
|
||||
@Path("/api/v1/dimension/admin/integrations")
|
||||
export class AdminIntegrationsService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@POST
|
||||
@Path(":category/:type/options")
|
||||
public async setOptions(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string, @PathParam("type") type: string, body: SetOptionsRequest): Promise<any> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async setOptions(@PathParam("category") category: string, @PathParam("type") type: string, body: SetOptionsRequest): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
if (category === "widget") await WidgetStore.setOptions(type, body.options);
|
||||
else throw new ApiError(400, "Unrecognized category");
|
||||
@ -39,8 +43,9 @@ export class AdminIntegrationsService {
|
||||
|
||||
@POST
|
||||
@Path(":category/:type/enabled")
|
||||
public async setEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string, @PathParam("type") type: string, body: SetEnabledRequest): Promise<any> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async setEnabled(@PathParam("category") category: string, @PathParam("type") type: string, body: SetEnabledRequest): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
if (category === "widget") await WidgetStore.setEnabled(type, body.enabled);
|
||||
else if (category === "bridge") await BridgeStore.setEnabled(type, body.enabled);
|
||||
@ -53,8 +58,9 @@ export class AdminIntegrationsService {
|
||||
|
||||
@GET
|
||||
@Path(":category/all")
|
||||
public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string): Promise<Integration[]> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getAllIntegrations(@PathParam("category") category: string): Promise<Integration[]> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
if (category === "widget") return await DimensionIntegrationsService.getWidgets(false);
|
||||
else if (category === "bridge") return await DimensionIntegrationsService.getBridges(false, userId);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_INTEGRATIONS, CACHE_IRC_BRIDGE } from "../../MemoryCache";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
@ -7,6 +6,7 @@ import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
|
||||
import { AvailableNetworks, IrcBridge } from "../../bridges/IrcBridge";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import IrcBridgeNetwork from "../../db/models/IrcBridgeNetwork";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreateWithUpstream {
|
||||
upstreamId: number;
|
||||
@ -35,10 +35,14 @@ interface SetEnabledRequest {
|
||||
@Path("/api/v1/dimension/admin/irc")
|
||||
export class AdminIrcService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridges(): Promise<BridgeResponse[]> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const bridges = await IrcBridgeRecord.findAll();
|
||||
const client = new IrcBridge(userId);
|
||||
@ -64,8 +68,9 @@ export class AdminIrcService {
|
||||
|
||||
@GET
|
||||
@Path(":bridgeId")
|
||||
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const ircBridge = await IrcBridgeRecord.findByPk(bridgeId);
|
||||
if (!ircBridge) throw new ApiError(404, "IRC Bridge not found");
|
||||
@ -91,8 +96,9 @@ export class AdminIrcService {
|
||||
|
||||
@POST
|
||||
@Path(":bridgeId/network/:networkId/enabled")
|
||||
public async setNetworkEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, @PathParam("networkId") networkId: string, request: SetEnabledRequest): Promise<any> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async setNetworkEnabled(@PathParam("bridgeId") bridgeId: number, @PathParam("networkId") networkId: string, request: SetEnabledRequest): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const ircBridge = await IrcBridgeRecord.findByPk(bridgeId);
|
||||
if (!ircBridge) throw new ApiError(404, "IRC Bridge not found");
|
||||
@ -116,8 +122,9 @@ export class AdminIrcService {
|
||||
|
||||
@POST
|
||||
@Path("new/upstream")
|
||||
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForUpstream(request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const upstream = await Upstream.findByPk(request.upstreamId);
|
||||
if (!upstream) throw new ApiError(400, "Upstream not found");
|
||||
@ -130,13 +137,14 @@ export class AdminIrcService {
|
||||
|
||||
Cache.for(CACHE_IRC_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/selfhosted")
|
||||
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const bridge = await IrcBridgeRecord.create({
|
||||
provisionUrl: request.provisionUrl,
|
||||
@ -146,6 +154,6 @@ export class AdminIrcService {
|
||||
|
||||
Cache.for(CACHE_IRC_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_INTEGRATIONS, CACHE_NEB } from "../../MemoryCache";
|
||||
import { NebStore } from "../../db/NebStore";
|
||||
import { NebConfig } from "../../models/neb";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreateWithUpstream {
|
||||
upstreamId: number;
|
||||
@ -26,11 +26,13 @@ interface SetEnabledRequest {
|
||||
@Path("/api/v1/dimension/admin/neb")
|
||||
export class AdminNebService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getNebConfigs(@QueryParam("scalar_token") scalarToken: string): Promise<NebConfig[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getNebConfigs(): Promise<NebConfig[]> {
|
||||
const cachedConfigs = Cache.for(CACHE_NEB).get("configurations");
|
||||
if (cachedConfigs) return cachedConfigs;
|
||||
|
||||
@ -41,8 +43,9 @@ export class AdminNebService {
|
||||
|
||||
@GET
|
||||
@Path(":id/config")
|
||||
public async getNebConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number): Promise<NebConfig> {
|
||||
const configs = await this.getNebConfigs(scalarToken); // does auth for us
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getNebConfig(@PathParam("id") nebId: number): Promise<NebConfig> {
|
||||
const configs = await this.getNebConfigs();
|
||||
const firstConfig = configs.filter(c => c.id === nebId)[0];
|
||||
if (!firstConfig) throw new ApiError(404, "Configuration not found");
|
||||
return firstConfig;
|
||||
@ -50,8 +53,9 @@ export class AdminNebService {
|
||||
|
||||
@POST
|
||||
@Path(":id/integration/:type/enabled")
|
||||
public async setIntegrationEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string, request: SetEnabledRequest): Promise<any> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async setIntegrationEnabled(@PathParam("id") nebId: number, @PathParam("type") integrationType: string, request: SetEnabledRequest): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
await NebStore.setIntegrationEnabled(nebId, integrationType, request.enabled);
|
||||
LogService.info("AdminNebService", userId + " set the " + integrationType + " on NEB " + nebId + " to " + (request.enabled ? "enabled" : "disabled"));
|
||||
@ -63,8 +67,9 @@ export class AdminNebService {
|
||||
|
||||
@POST
|
||||
@Path(":id/integration/:type/config")
|
||||
public async setIntegrationConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async setIntegrationConfig(@PathParam("id") nebId: number, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
await NebStore.setIntegrationConfig(nebId, integrationType, newConfig);
|
||||
LogService.info("AdminNebService", userId + " updated the configuration for " + integrationType + " on NEB " + nebId);
|
||||
@ -76,15 +81,16 @@ export class AdminNebService {
|
||||
|
||||
@GET
|
||||
@Path(":id/integration/:type/config")
|
||||
public async getIntegrationConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string): Promise<any> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getIntegrationConfig(@PathParam("id") nebId: number, @PathParam("type") integrationType: string): Promise<any> {
|
||||
return NebStore.getIntegrationConfig(nebId, integrationType);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/upstream")
|
||||
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<NebConfig> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForUpstream(request: CreateWithUpstream): Promise<NebConfig> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const neb = await NebStore.createForUpstream(request.upstreamId);
|
||||
@ -101,8 +107,9 @@ export class AdminNebService {
|
||||
|
||||
@POST
|
||||
@Path("new/appservice")
|
||||
public async newConfigForAppservice(@QueryParam("scalar_token") scalarToken: string, request: CreateWithAppservice): Promise<NebConfig> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForAppservice(request: CreateWithAppservice): Promise<NebConfig> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const neb = await NebStore.createForAppservice(request.appserviceId, request.adminUrl);
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
||||
import { GET, Path, POST, QueryParam, Security } from "typescript-rest";
|
||||
import config from "../../config";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
||||
import { CURRENT_VERSION } from "../../version";
|
||||
import { getFederationConnInfo } from "../../matrix/helpers";
|
||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface DimensionVersionResponse {
|
||||
version: string;
|
||||
@ -33,49 +32,24 @@ interface DimensionConfigResponse {
|
||||
@Path("/api/v1/dimension/admin")
|
||||
export class AdminService {
|
||||
|
||||
/**
|
||||
* Determines if a given user is an administrator
|
||||
* @param {string} userId The user ID to validate
|
||||
* @returns {boolean} True if the user is an administrator
|
||||
*/
|
||||
public static isAdmin(userId: string) {
|
||||
return config.admins.indexOf(userId) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given scalar token to ensure the owner is an administrator. If the
|
||||
* given scalar token does not belong to an administrator, an ApiError is raised.
|
||||
* @param {string} scalarToken The scalar token to validate
|
||||
* @returns {Promise<string>} Resolves to the owner's user ID if they are an administrator
|
||||
* @throws {ApiError} Thrown with a status code of 401 if the owner is not an administrator
|
||||
*/
|
||||
public static async validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
|
||||
const accountController = new AccountController();
|
||||
const userId = await accountController.getTokenOwner(scalarToken, true);
|
||||
if (!AdminService.isAdmin(userId))
|
||||
throw new ApiError(401, "You must be an administrator to use this API");
|
||||
return userId;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("check")
|
||||
public async checkIfAdmin(@QueryParam("scalar_token") scalarToken: string): Promise<{}> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async checkIfAdmin(): Promise<{}> {
|
||||
return {}; // A 200 OK essentially means "you're an admin".
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("version")
|
||||
public async getVersion(@QueryParam("scalar_token") scalarToken: string): Promise<DimensionVersionResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getVersion(): Promise<DimensionVersionResponse> {
|
||||
return {version: CURRENT_VERSION};
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("config")
|
||||
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<DimensionConfigResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getConfig(): Promise<DimensionConfigResponse> {
|
||||
const client = new MatrixLiteClient(config.homeserver.accessToken);
|
||||
const fedInfo = await getFederationConnInfo(config.homeserver.name);
|
||||
return {
|
||||
@ -96,9 +70,8 @@ export class AdminService {
|
||||
|
||||
@GET
|
||||
@Path("test/federation")
|
||||
public async testFederationRouting(@QueryParam("scalar_token") scalarToken: string, @QueryParam("server_name") serverName: string): Promise<any> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async testFederationRouting(@QueryParam("server_name") serverName: string): Promise<any> {
|
||||
return {
|
||||
inputServerName: serverName,
|
||||
resolvedServer: await getFederationConnInfo(serverName),
|
||||
@ -107,9 +80,8 @@ export class AdminService {
|
||||
|
||||
@POST
|
||||
@Path("sessions/logout/all")
|
||||
public async logoutAll(@QueryParam("scalar_token") scalarToken: string): Promise<any> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async logoutAll(): Promise<any> {
|
||||
// Clear the cache first to hopefully invalidate a bunch of them
|
||||
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_INTEGRATIONS, CACHE_SLACK_BRIDGE } from "../../MemoryCache";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import SlackBridgeRecord from "../../db/models/SlackBridgeRecord";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreateWithUpstream {
|
||||
upstreamId: number;
|
||||
@ -27,11 +27,13 @@ interface BridgeResponse {
|
||||
@Path("/api/v1/dimension/admin/slack")
|
||||
export class AdminSlackService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridges(): Promise<BridgeResponse[]> {
|
||||
const bridges = await SlackBridgeRecord.findAll();
|
||||
return Promise.all(bridges.map(async b => {
|
||||
return {
|
||||
@ -45,9 +47,8 @@ export class AdminSlackService {
|
||||
|
||||
@GET
|
||||
@Path(":bridgeId")
|
||||
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
const telegramBridge = await SlackBridgeRecord.findByPk(bridgeId);
|
||||
if (!telegramBridge) throw new ApiError(404, "Slack Bridge not found");
|
||||
|
||||
@ -61,9 +62,9 @@ export class AdminSlackService {
|
||||
|
||||
@POST
|
||||
@Path(":bridgeId")
|
||||
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const bridge = await SlackBridgeRecord.findByPk(bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
|
||||
@ -74,14 +75,14 @@ export class AdminSlackService {
|
||||
|
||||
Cache.for(CACHE_SLACK_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/upstream")
|
||||
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForUpstream(request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const upstream = await Upstream.findByPk(request.upstreamId);
|
||||
if (!upstream) throw new ApiError(400, "Upstream not found");
|
||||
|
||||
@ -93,14 +94,14 @@ export class AdminSlackService {
|
||||
|
||||
Cache.for(CACHE_SLACK_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/selfhosted")
|
||||
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const bridge = await SlackBridgeRecord.create({
|
||||
provisionUrl: request.provisionUrl,
|
||||
isEnabled: true,
|
||||
@ -109,6 +110,6 @@ export class AdminSlackService {
|
||||
|
||||
Cache.for(CACHE_SLACK_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import StickerPack from "../../db/models/StickerPack";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService";
|
||||
@ -10,6 +9,7 @@ import config from "../../config";
|
||||
import Sticker from "../../db/models/Sticker";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import * as sharp from "sharp";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface SetEnabledRequest {
|
||||
isEnabled: boolean;
|
||||
@ -25,17 +25,20 @@ interface ImportTelegramRequest {
|
||||
@Path("/api/v1/dimension/admin/stickers")
|
||||
export class AdminStickerService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("packs")
|
||||
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getStickerPacks(): Promise<MemoryStickerPack[]> {
|
||||
return await DimensionStickerService.getStickerPacks(false);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("packs/:id/enabled")
|
||||
public async setPackEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") packId: number, request: SetEnabledRequest): Promise<any> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async setPackEnabled(@PathParam("id") packId: number, request: SetEnabledRequest): Promise<any> {
|
||||
const pack = await StickerPack.findByPk(packId);
|
||||
if (!pack) throw new ApiError(404, "Sticker pack not found");
|
||||
|
||||
@ -48,8 +51,9 @@ export class AdminStickerService {
|
||||
|
||||
@POST
|
||||
@Path("packs/import/telegram")
|
||||
public async importFromTelegram(@QueryParam("scalar_token") scalarToken: string, request: ImportTelegramRequest): Promise<MemoryStickerPack> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async importFromTelegram(request: ImportTelegramRequest): Promise<MemoryStickerPack> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
if (!request.packUrl || (!request.packUrl.startsWith("https://t.me/addstickers/") && !request.packUrl.startsWith("https://telegram.me/addstickers/"))) {
|
||||
throw new ApiError(400, "Invalid pack URL");
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_INTEGRATIONS, CACHE_TELEGRAM_BRIDGE } from "../../MemoryCache";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import TelegramBridgeRecord from "../../db/models/TelegramBridgeRecord";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreateWithUpstream {
|
||||
upstreamId: number;
|
||||
@ -32,11 +32,13 @@ interface BridgeResponse {
|
||||
@Path("/api/v1/dimension/admin/telegram")
|
||||
export class AdminTelegramService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridges(): Promise<BridgeResponse[]> {
|
||||
const bridges = await TelegramBridgeRecord.findAll();
|
||||
return Promise.all(bridges.map(async b => {
|
||||
return {
|
||||
@ -53,9 +55,8 @@ export class AdminTelegramService {
|
||||
|
||||
@GET
|
||||
@Path(":bridgeId")
|
||||
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
const telegramBridge = await TelegramBridgeRecord.findByPk(bridgeId);
|
||||
if (!telegramBridge) throw new ApiError(404, "Telegram Bridge not found");
|
||||
|
||||
@ -72,8 +73,9 @@ export class AdminTelegramService {
|
||||
|
||||
@POST
|
||||
@Path(":bridgeId")
|
||||
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const bridge = await TelegramBridgeRecord.findByPk(bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
@ -88,19 +90,21 @@ export class AdminTelegramService {
|
||||
|
||||
Cache.for(CACHE_TELEGRAM_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/upstream")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
throw new ApiError(400, "Cannot create a telegram bridge from an upstream");
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/selfhosted")
|
||||
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const bridge = await TelegramBridgeRecord.create({
|
||||
provisionUrl: request.provisionUrl,
|
||||
@ -113,6 +117,6 @@ export class AdminTelegramService {
|
||||
|
||||
Cache.for(CACHE_TELEGRAM_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
}
|
56
src/api/admin/AdminTermsService.ts
Normal file
56
src/api/admin/AdminTermsService.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { GET, Path, PathParam, POST, PUT, Security } from "typescript-rest";
|
||||
import TermsController, { ITerms } from "../controllers/TermsController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreatePolicyObject {
|
||||
name: string;
|
||||
text?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Administrative API for configuring terms of service.
|
||||
*/
|
||||
@Path("/api/v1/dimension/admin/terms")
|
||||
@AutoWired
|
||||
export class AdminTermsService {
|
||||
|
||||
@Inject
|
||||
private termsController: TermsController;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getPolicies(): Promise<ITerms[]> {
|
||||
return this.termsController.getPoliciesForAdmin();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path(":shortcode/:version")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getPolicy(@PathParam("shortcode") shortcode: string, @PathParam("version") version: string): Promise<ITerms> {
|
||||
return this.termsController.getPolicyForAdmin(shortcode, version);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(":shortcode/draft")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async createDraftPolicy(@PathParam("shortcode") shortcode: string, request: CreatePolicyObject): Promise<ITerms> {
|
||||
return this.termsController.createDraftPolicy(request.name, shortcode, request.text, request.url);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path(":shortcode/publish/:version")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async publishDraftPolicy(@PathParam("shortcode") shortcode: string, @PathParam("version") version: string): Promise<ITerms> {
|
||||
return this.termsController.publishPolicy(shortcode, version);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path(":shortcode/:version")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async updatePolicy(@PathParam("shortcode") shortcode: string, @PathParam("version") version: string, request: CreatePolicyObject): Promise<ITerms> {
|
||||
return this.termsController.updatePolicy(request.name, shortcode, version, request.text, request.url);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_SCALAR_ACCOUNTS, CACHE_UPSTREAM } from "../../MemoryCache";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface UpstreamRepsonse {
|
||||
id: number;
|
||||
@ -26,11 +26,13 @@ interface NewUpstreamRequest {
|
||||
@Path("/api/v1/dimension/admin/upstreams")
|
||||
export class AdminUpstreamService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getUpstreams(@QueryParam("scalar_token") scalarToken: string): Promise<UpstreamRepsonse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getUpstreams(): Promise<UpstreamRepsonse[]> {
|
||||
const cachedUpstreams = Cache.for(CACHE_UPSTREAM).get("upstreams");
|
||||
if (cachedUpstreams) return cachedUpstreams;
|
||||
|
||||
@ -42,8 +44,9 @@ export class AdminUpstreamService {
|
||||
|
||||
@POST
|
||||
@Path("new")
|
||||
public async createUpstream(@QueryParam("scalar_token") scalarToken: string, request: NewUpstreamRequest): Promise<UpstreamRepsonse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async createUpstream(request: NewUpstreamRequest): Promise<UpstreamRepsonse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const upstream = await Upstream.create({
|
||||
name: request.name,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { AdminService } from "./AdminService";
|
||||
import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_INTEGRATIONS, CACHE_WEBHOOKS_BRIDGE } from "../../MemoryCache";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ApiError } from "../ApiError";
|
||||
import WebhookBridgeRecord from "../../db/models/WebhookBridgeRecord";
|
||||
import { ROLE_ADMIN, ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface CreateWithUpstream {
|
||||
upstreamId: number;
|
||||
@ -28,11 +28,13 @@ interface BridgeResponse {
|
||||
@Path("/api/v1/dimension/admin/webhooks")
|
||||
export class AdminWebhooksService {
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("all")
|
||||
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridges(): Promise<BridgeResponse[]> {
|
||||
const bridges = await WebhookBridgeRecord.findAll();
|
||||
return Promise.all(bridges.map(async b => {
|
||||
return {
|
||||
@ -47,9 +49,8 @@ export class AdminWebhooksService {
|
||||
|
||||
@GET
|
||||
@Path(":bridgeId")
|
||||
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
|
||||
const webhookBridge = await WebhookBridgeRecord.findByPk(bridgeId);
|
||||
if (!webhookBridge) throw new ApiError(404, "Webhook Bridge not found");
|
||||
|
||||
@ -64,8 +65,9 @@ export class AdminWebhooksService {
|
||||
|
||||
@POST
|
||||
@Path(":bridgeId")
|
||||
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const bridge = await WebhookBridgeRecord.findByPk(bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
@ -78,19 +80,21 @@ export class AdminWebhooksService {
|
||||
|
||||
Cache.for(CACHE_WEBHOOKS_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/upstream")
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
|
||||
throw new ApiError(400, "Cannot create a webhook bridge from an upstream");
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("new/selfhosted")
|
||||
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken);
|
||||
@Security([ROLE_USER, ROLE_ADMIN])
|
||||
public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const bridge = await WebhookBridgeRecord.create({
|
||||
provisionUrl: request.provisionUrl,
|
||||
@ -101,6 +105,6 @@ export class AdminWebhooksService {
|
||||
|
||||
Cache.for(CACHE_WEBHOOKS_BRIDGE).clear();
|
||||
Cache.for(CACHE_INTEGRATIONS).clear();
|
||||
return this.getBridge(scalarToken, bridge.id);
|
||||
return this.getBridge(bridge.id);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
import * as randomString from "random-string";
|
||||
import { AutoWired } from "typescript-ioc/es6";
|
||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
||||
import { IMSCUser } from "../security/MSCSecurity";
|
||||
import { ILoggedInUser } from "../security/MatrixSecurity";
|
||||
|
||||
export interface IAccountRegisteredResponse {
|
||||
token: string;
|
||||
@ -31,16 +31,15 @@ export default class AccountController {
|
||||
/**
|
||||
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
|
||||
* @param {string} scalarToken The scalar token to validate
|
||||
* @param {boolean} ignoreUpstreams True to consider the token valid if it is missing links to other upstreams
|
||||
* @returns {Promise<string>} Resolves to the owner's user ID if the token is valid.
|
||||
* @throws {ApiError} Thrown with a status code of 401 if the token is invalid.
|
||||
*/
|
||||
public async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> {
|
||||
public async getTokenOwner(scalarToken: string): Promise<string> {
|
||||
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
|
||||
if (cachedUserId) return cachedUserId;
|
||||
|
||||
try {
|
||||
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
|
||||
const user = await ScalarStore.getTokenOwner(scalarToken);
|
||||
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
|
||||
return user.userId;
|
||||
} catch (err) {
|
||||
@ -52,9 +51,10 @@ export default class AccountController {
|
||||
/**
|
||||
* Registers an account to use the Integration Manager
|
||||
* @param {OpenId} openId The OpenID request information.
|
||||
* @param {string} scalarKind The kind of scalar client to use.
|
||||
* @returns {Promise<IAccountRegisteredResponse>} Resolves when registered.
|
||||
*/
|
||||
public async registerAccount(openId: OpenId): Promise<IAccountRegisteredResponse> {
|
||||
public async registerAccount(openId: OpenId, scalarKind: string): Promise<IAccountRegisteredResponse> {
|
||||
if (!openId || !openId.matrix_server_name || !openId.access_token) {
|
||||
throw new ApiError(400, "Missing OpenID information");
|
||||
}
|
||||
@ -77,7 +77,7 @@ export default class AccountController {
|
||||
|
||||
const upstreams = await Upstream.findAll();
|
||||
await Promise.all(upstreams.map(async upstream => {
|
||||
if (!await ScalarStore.isUpstreamOnline(upstream)) {
|
||||
if (!await ScalarStore.isUpstreamOnline(upstream, scalarKind)) {
|
||||
LogService.warn("AccountController", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
|
||||
return null;
|
||||
}
|
||||
@ -108,13 +108,17 @@ export default class AccountController {
|
||||
|
||||
/**
|
||||
* Logs a user out
|
||||
* @param {IMSCUser} user The user to log out
|
||||
* @param {ILoggedInUser} user The user to log out
|
||||
* @returns {Promise<*>} Resolves when complete.
|
||||
*/
|
||||
public async logout(user: IMSCUser): Promise<any> {
|
||||
// TODO: Create a link to upstream tokens to log them out too
|
||||
const tokens = await UserScalarToken.findAll({where: {scalarToken: user.token}});
|
||||
public async logout(user: ILoggedInUser): Promise<any> {
|
||||
const tokens = await UserScalarToken.findAll({where: {scalarToken: user.token}, include: [Upstream]});
|
||||
for (const token of tokens) {
|
||||
if (token.upstream) {
|
||||
LogService.info("AccountController", "Logging user out of upstream");
|
||||
const client = new ScalarClient(token.upstream, ScalarClient.KIND_MATRIX_V1);
|
||||
await client.logout(token.scalarToken);
|
||||
}
|
||||
await token.destroy();
|
||||
}
|
||||
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
|
||||
|
274
src/api/controllers/TermsController.ts
Normal file
274
src/api/controllers/TermsController.ts
Normal file
@ -0,0 +1,274 @@
|
||||
import { AutoWired } from "typescript-ioc/es6";
|
||||
import { ILoggedInUser } from "../security/MatrixSecurity";
|
||||
import TermsRecord from "../../db/models/TermsRecord";
|
||||
import TermsTextRecord from "../../db/models/TermsTextRecord";
|
||||
import TermsSignedRecord from "../../db/models/TermsSignedRecord";
|
||||
import { Op } from "sequelize";
|
||||
import { Cache, CACHE_TERMS } from "../../MemoryCache";
|
||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||
import Upstream from "../../db/models/Upstream";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
import { md5 } from "../../utils/hashing";
|
||||
import TermsUpstreamRecord from "../../db/models/TermsUpstreamRecord";
|
||||
|
||||
export interface ILanguagePolicy {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IPolicy {
|
||||
version: string;
|
||||
|
||||
// a string value is not allowed here, but TypeScript is angry otherwise.
|
||||
[language: string]: string | ILanguagePolicy;
|
||||
}
|
||||
|
||||
export interface ITermsResponse {
|
||||
policies: { [policyName: string]: IPolicy };
|
||||
}
|
||||
|
||||
export interface ITerms {
|
||||
shortcode: string;
|
||||
version: string;
|
||||
languages: {
|
||||
[lang: string]: {
|
||||
name: string;
|
||||
url: string;
|
||||
text?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICachedTerms extends ITerms {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const VERSION_DRAFT = "draft";
|
||||
|
||||
/**
|
||||
* API controller for terms of service management
|
||||
*/
|
||||
@AutoWired
|
||||
export default class TermsController {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
private async getPublishedTerms(): Promise<ICachedTerms[]> {
|
||||
const cache = Cache.for(CACHE_TERMS);
|
||||
|
||||
let terms = cache.get("published");
|
||||
if (terms) return terms;
|
||||
|
||||
terms = (await TermsRecord.findAll({
|
||||
where: {version: {[Op.ne]: VERSION_DRAFT}},
|
||||
include: [TermsTextRecord],
|
||||
}));
|
||||
|
||||
const latest: { [shortcode: string]: TermsRecord } = {};
|
||||
for (const record of terms) {
|
||||
if (!latest[record.shortcode]) {
|
||||
latest[record.shortcode] = record;
|
||||
}
|
||||
if (latest[record.shortcode].id < record.id) {
|
||||
latest[record.shortcode] = record;
|
||||
}
|
||||
}
|
||||
|
||||
terms = Object.values(latest).map(p => {
|
||||
const mapped = this.mapPolicy(false, p);
|
||||
return <ICachedTerms>Object.assign({}, mapped, {
|
||||
id: p.id,
|
||||
});
|
||||
});
|
||||
cache.put("published", terms);
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
||||
public async doesUserNeedToSignTerms(user: ILoggedInUser): Promise<boolean> {
|
||||
const latest = await this.getPublishedTerms();
|
||||
const signed = await TermsSignedRecord.findAll({where: {userId: user.userId}});
|
||||
|
||||
const missing = Object.values(latest).filter(d => !signed.find(s => s.termsId === d.id));
|
||||
if (missing.length > 0) return true;
|
||||
|
||||
// Test upstream terms for the user
|
||||
const tokensForUser = await UserScalarToken.findAll({where: {userId: user.userId}, include: [Upstream]});
|
||||
const upstreamTokens = tokensForUser.filter(t => t.upstream);
|
||||
for (const upstreamToken of upstreamTokens) {
|
||||
try {
|
||||
const scalarClient = new ScalarClient(upstreamToken.upstream, ScalarClient.KIND_MATRIX_V1);
|
||||
await scalarClient.getAccount(upstreamToken.scalarToken);
|
||||
// 200 OK means we're fine
|
||||
} catch (e) {
|
||||
if (e.statusCode === 403 && e.body && e.body.errcode === 'M_TERMS_NOT_SIGNED') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async getAvailableTerms(): Promise<ITermsResponse> {
|
||||
const latest = await this.getPublishedTerms();
|
||||
const policies: ITermsResponse = {policies: {}};
|
||||
|
||||
for (const termsPolicy of Object.values(latest)) {
|
||||
policies.policies[termsPolicy.shortcode] = {
|
||||
version: termsPolicy.version,
|
||||
};
|
||||
|
||||
for (const language in termsPolicy.languages) {
|
||||
policies.policies[termsPolicy.shortcode][language] = {
|
||||
name: termsPolicy.languages[language].name,
|
||||
url: termsPolicy.languages[language].url,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get upstream terms
|
||||
const usptreams = await Upstream.findAll();
|
||||
const urlsToUpstream = {}; // {url: [upstreamId]}
|
||||
for (const upstream of usptreams) {
|
||||
try {
|
||||
const scalarClient = new ScalarClient(upstream, ScalarClient.KIND_MATRIX_V1);
|
||||
const upstreamTerms = await scalarClient.getAvailableTerms();
|
||||
|
||||
// rewrite the shortcodes to avoid conflicts
|
||||
const shortcodePrefix = `upstream_${md5(`${upstream.id}:${upstream.apiUrl}`)}`;
|
||||
for (const shortcode of Object.keys(upstreamTerms.policies)) {
|
||||
policies.policies[`${shortcodePrefix}_${shortcode}`] = upstreamTerms.policies[shortcode];
|
||||
|
||||
// copy all urls for later adding to the database
|
||||
for (const language of Object.keys(upstreamTerms.policies[shortcode])) {
|
||||
const upstreamUrl = upstreamTerms.policies[shortcode][language]['url'];
|
||||
if (!urlsToUpstream[upstreamUrl]) urlsToUpstream[upstreamUrl] = [];
|
||||
const upstreamsArr = urlsToUpstream[upstreamUrl];
|
||||
if (!upstreamsArr.includes(upstream.id)) {
|
||||
upstreamsArr.push(upstream.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.error("TermsController", e);
|
||||
}
|
||||
}
|
||||
|
||||
// actually cache the urls in the database
|
||||
const existingCache = await TermsUpstreamRecord.findAll({where: {url: {[Op.in]: Object.keys(urlsToUpstream)}}});
|
||||
for (const upstreamUrl of Object.keys(urlsToUpstream)) {
|
||||
const upstreamIds = urlsToUpstream[upstreamUrl];
|
||||
const existingIds = existingCache.filter(c => c.url === upstreamUrl).map(c => c.upstreamId);
|
||||
const missingIds = upstreamIds.filter(i => !existingIds.includes(i));
|
||||
for (const targetUpstreamId of missingIds) {
|
||||
const item = await TermsUpstreamRecord.create({url: upstreamUrl, upstreamId: targetUpstreamId});
|
||||
existingCache.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return policies;
|
||||
}
|
||||
|
||||
public async signTermsMatching(user: ILoggedInUser, urls: string[]): Promise<any> {
|
||||
const terms = await TermsTextRecord.findAll({where: {url: {[Op.in]: urls}}});
|
||||
const signed = await TermsSignedRecord.findAll({where: {userId: user.userId}});
|
||||
|
||||
const toAdd = terms.filter(t => !signed.find(s => s.termsId === t.termsId));
|
||||
for (const termsToSign of toAdd) {
|
||||
await TermsSignedRecord.create({termsId: termsToSign.id, userId: user.userId});
|
||||
}
|
||||
|
||||
// Check upstreams too, if there are any
|
||||
const upstreamPolicies = await TermsUpstreamRecord.findAll({
|
||||
where: {url: {[Op.in]: urls}},
|
||||
include: [Upstream]
|
||||
});
|
||||
const upstreamsToSignatures: { [upstreamId: number]: { upstream: Upstream, token: string, urls: string[] } } = {};
|
||||
for (const upstreamPolicy of upstreamPolicies) {
|
||||
const userToken = await UserScalarToken.findOne({
|
||||
where: {
|
||||
upstreamId: upstreamPolicy.upstreamId,
|
||||
userId: user.userId,
|
||||
},
|
||||
});
|
||||
if (!userToken) {
|
||||
LogService.warn("TermsController", `User ${user.userId} is missing an upstream token for ${upstreamPolicy.upstream.scalarUrl}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!upstreamsToSignatures[upstreamPolicy.upstreamId]) upstreamsToSignatures[upstreamPolicy.upstreamId] = {
|
||||
upstream: upstreamPolicy.upstream,
|
||||
token: userToken.scalarToken,
|
||||
urls: [],
|
||||
};
|
||||
upstreamsToSignatures[upstreamPolicy.upstreamId].urls.push(upstreamPolicy.url);
|
||||
}
|
||||
|
||||
for (const upstreamSignature of Object.values(upstreamsToSignatures)) {
|
||||
const client = new ScalarClient(upstreamSignature.upstream, ScalarClient.KIND_MATRIX_V1);
|
||||
await client.signTermsUrls(upstreamSignature.token, upstreamSignature.urls);
|
||||
}
|
||||
}
|
||||
|
||||
public async getPoliciesForAdmin(): Promise<ITerms[]> {
|
||||
const terms = await TermsRecord.findAll({include: [TermsTextRecord]});
|
||||
return terms.map(this.mapPolicy.bind(this, false));
|
||||
}
|
||||
|
||||
public async getPolicyForAdmin(shortcode: string, version: string): Promise<ITerms> {
|
||||
const terms = await TermsRecord.findOne({where: {shortcode, version}, include: [TermsTextRecord]});
|
||||
return this.mapPolicy(true, terms);
|
||||
}
|
||||
|
||||
public async createDraftPolicy(name: string, shortcode: string, text: string, url: string): Promise<ITerms> {
|
||||
const terms = await TermsRecord.create({shortcode, version: VERSION_DRAFT});
|
||||
const termsText = await TermsTextRecord.create({termsId: terms.id, language: "en", name, text, url});
|
||||
|
||||
terms.texts = [termsText];
|
||||
return this.mapPolicy(true, terms);
|
||||
}
|
||||
|
||||
public async updatePolicy(name: string, shortcode: string, version: string, text: string, url: string): Promise<ITerms> {
|
||||
const terms = await TermsRecord.findOne({where: {shortcode, version}, include: [TermsTextRecord]});
|
||||
const termsText = terms.texts.find(e => e.language === "en");
|
||||
|
||||
termsText.url = url;
|
||||
termsText.text = text;
|
||||
termsText.name = name;
|
||||
|
||||
await termsText.save();
|
||||
Cache.for(CACHE_TERMS).clear();
|
||||
|
||||
return this.mapPolicy(true, terms);
|
||||
}
|
||||
|
||||
public async publishPolicy(shortcode: string, targetVersion: string): Promise<ITerms> {
|
||||
const terms = await TermsRecord.findOne({
|
||||
where: {shortcode, version: VERSION_DRAFT},
|
||||
include: [TermsTextRecord],
|
||||
});
|
||||
if (!terms) throw new Error("Missing terms");
|
||||
|
||||
terms.version = targetVersion;
|
||||
await terms.save();
|
||||
Cache.for(CACHE_TERMS).clear();
|
||||
|
||||
return this.mapPolicy(true, terms);
|
||||
}
|
||||
|
||||
private mapPolicy(withText: boolean, policy: TermsRecord): ITerms {
|
||||
const languages = {};
|
||||
policy.texts.forEach(pt => languages[pt.language] = {
|
||||
name: pt.name,
|
||||
url: pt.url,
|
||||
text: withText ? pt.text : null,
|
||||
});
|
||||
return {
|
||||
shortcode: policy.shortcode,
|
||||
version: policy.version,
|
||||
languages: languages,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface BridgeRoomRequest {
|
||||
gitterRoomName: string;
|
||||
@ -13,17 +12,16 @@ interface BridgeRoomRequest {
|
||||
* API for interacting with the Gitter bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/gitter")
|
||||
@AutoWired
|
||||
export class DimensionGitterService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId/link")
|
||||
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedRoom> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async getLink(@PathParam("roomId") roomId: string): Promise<BridgedRoom> {
|
||||
const userId = this.context.request.user.userId;
|
||||
try {
|
||||
const gitter = new GitterBridge(userId);
|
||||
return gitter.getLink(roomId);
|
||||
@ -35,9 +33,9 @@ export class DimensionGitterService {
|
||||
|
||||
@POST
|
||||
@Path("room/:roomId/link")
|
||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
|
||||
const userId = this.context.request.user.userId;
|
||||
try {
|
||||
const gitter = new GitterBridge(userId);
|
||||
await gitter.requestLink(roomId, request.gitterRoomName);
|
||||
@ -50,9 +48,9 @@ export class DimensionGitterService {
|
||||
|
||||
@DELETE
|
||||
@Path("room/:roomId/link")
|
||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
try {
|
||||
const gitter = new GitterBridge(userId);
|
||||
const link = await gitter.getLink(roomId);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Widget } from "../../integrations/Widget";
|
||||
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||
import { Integration } from "../../integrations/Integration";
|
||||
@ -10,8 +10,7 @@ import { ComplexBot } from "../../integrations/ComplexBot";
|
||||
import { Bridge } from "../../integrations/Bridge";
|
||||
import { BridgeStore } from "../../db/BridgeStore";
|
||||
import { BotStore } from "../../db/BotStore";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
export interface IntegrationsResponse {
|
||||
widgets: Widget[],
|
||||
@ -24,11 +23,10 @@ export interface IntegrationsResponse {
|
||||
* API for managing integrations, primarily for a given room
|
||||
*/
|
||||
@Path("/api/v1/dimension/integrations")
|
||||
@AutoWired
|
||||
export class DimensionIntegrationsService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
/**
|
||||
* Gets a list of widgets
|
||||
@ -90,8 +88,9 @@ export class DimensionIntegrationsService {
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId")
|
||||
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async getIntegrationsInRoom(@PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
return {
|
||||
widgets: await DimensionIntegrationsService.getWidgets(true),
|
||||
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
||||
@ -102,8 +101,9 @@ export class DimensionIntegrationsService {
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId/integrations/:category/:type")
|
||||
public async getIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
|
||||
const roomConfig = await this.getIntegrationsInRoom(scalarToken, roomId); // does auth for us
|
||||
@Security(ROLE_USER)
|
||||
public async getIntegrationInRoom(@PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
|
||||
const roomConfig = await this.getIntegrationsInRoom(roomId); // does auth for us
|
||||
|
||||
if (category === "widget") return roomConfig.widgets.find(i => i.type === integrationType);
|
||||
else if (category === "bot") return roomConfig.bots.find(i => i.type === integrationType);
|
||||
@ -114,8 +114,9 @@ export class DimensionIntegrationsService {
|
||||
|
||||
@POST
|
||||
@Path("room/:roomId/integrations/:category/:type/config")
|
||||
public async setIntegrationConfigurationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async setIntegrationConfigurationInRoom(@PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
|
||||
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
|
||||
@ -127,8 +128,9 @@ export class DimensionIntegrationsService {
|
||||
|
||||
@DELETE
|
||||
@Path("room/:roomId/integrations/:category/:type")
|
||||
public async removeIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async removeIntegrationInRoom(@PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
|
||||
else if (category === "bot") {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { IrcBridge } from "../../bridges/IrcBridge";
|
||||
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
|
||||
import { ApiError } from "../ApiError";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface RequestLinkRequest {
|
||||
op: string;
|
||||
@ -14,17 +13,16 @@ interface RequestLinkRequest {
|
||||
* API for interacting with the IRC bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/irc")
|
||||
@AutoWired
|
||||
export class DimensionIrcService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path(":networkId/channel/:channel/ops")
|
||||
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async getOps(@PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||
const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
@ -38,9 +36,9 @@ export class DimensionIrcService {
|
||||
|
||||
@POST
|
||||
@Path(":networkId/channel/:channel/link/:roomId")
|
||||
public async requestLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async requestLink(@PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||
const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
@ -54,9 +52,9 @@ export class DimensionIrcService {
|
||||
|
||||
@POST
|
||||
@Path(":networkId/channel/:channel/unlink/:roomId")
|
||||
public async unlink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async unlink(@PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||
const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId);
|
||||
if (!bridge) throw new ApiError(404, "Bridge not found");
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge";
|
||||
import { SlackChannel, SlackTeam } from "../../bridges/models/slack";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface BridgeRoomRequest {
|
||||
teamId: string;
|
||||
@ -15,16 +14,16 @@ interface BridgeRoomRequest {
|
||||
* API for interacting with the Slack bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/slack")
|
||||
@AutoWired
|
||||
export class DimensionSlackService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("room/:roomId/link")
|
||||
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedChannel> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async getLink(@PathParam("roomId") roomId: string): Promise<BridgedChannel> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
@ -37,8 +36,9 @@ export class DimensionSlackService {
|
||||
|
||||
@POST
|
||||
@Path("room/:roomId/link")
|
||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
@ -52,8 +52,8 @@ export class DimensionSlackService {
|
||||
|
||||
@DELETE
|
||||
@Path("room/:roomId/link")
|
||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
@ -69,8 +69,9 @@ export class DimensionSlackService {
|
||||
|
||||
@GET
|
||||
@Path("teams")
|
||||
public async getTeams(@QueryParam("scalar_token") scalarToken: string): Promise<SlackTeam[]> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async getTeams(): Promise<SlackTeam[]> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
const slack = new SlackBridge(userId);
|
||||
const teams = await slack.getTeams();
|
||||
@ -80,8 +81,9 @@ export class DimensionSlackService {
|
||||
|
||||
@GET
|
||||
@Path("teams/:teamId/channels")
|
||||
public async getChannels(@QueryParam("scalar_token") scalarToken: string, @PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async getChannels(@PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
@ -94,8 +96,9 @@ export class DimensionSlackService {
|
||||
|
||||
@GET
|
||||
@Path("auth")
|
||||
public async getAuthUrl(@QueryParam("scalar_token") scalarToken: string): Promise<{ authUrl: string }> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async getAuthUrl(): Promise<{ authUrl: string }> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const slack = new SlackBridge(userId);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
|
||||
import StickerPack from "../../db/models/StickerPack";
|
||||
import Sticker from "../../db/models/Sticker";
|
||||
@ -7,8 +7,7 @@ import { ApiError } from "../ApiError";
|
||||
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
|
||||
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
|
||||
import config from "../../config";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
export interface MemoryStickerPack {
|
||||
id: number;
|
||||
@ -64,11 +63,10 @@ interface StickerConfig {
|
||||
* API for stickers
|
||||
*/
|
||||
@Path("/api/v1/dimension/stickers")
|
||||
@AutoWired
|
||||
export class DimensionStickerService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> {
|
||||
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
|
||||
@ -90,9 +88,8 @@ export class DimensionStickerService {
|
||||
|
||||
@GET
|
||||
@Path("config")
|
||||
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<StickerConfig> {
|
||||
await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async getConfig(): Promise<StickerConfig> {
|
||||
return {
|
||||
enabled: config.stickers.enabled,
|
||||
stickerBot: config.stickers.stickerBot,
|
||||
@ -102,9 +99,9 @@ export class DimensionStickerService {
|
||||
|
||||
@GET
|
||||
@Path("packs")
|
||||
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async getStickerPacks(): Promise<MemoryStickerPack[]> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
|
||||
if (cachedPacks) return cachedPacks;
|
||||
|
||||
@ -129,9 +126,9 @@ export class DimensionStickerService {
|
||||
|
||||
@POST
|
||||
@Path("packs/:packId/selected")
|
||||
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async setPackSelected(@PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const pack = await StickerPack.findByPk(packId);
|
||||
if (!pack) throw new ApiError(404, "Sticker pack not found");
|
||||
|
||||
@ -153,9 +150,8 @@ export class DimensionStickerService {
|
||||
|
||||
@POST
|
||||
@Path("packs/import")
|
||||
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> {
|
||||
await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async importPack(request: ImportPackRequest): Promise<MemoryUserStickerPack> {
|
||||
if (!config.stickers.enabled) {
|
||||
throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver");
|
||||
}
|
||||
@ -171,7 +167,7 @@ export class DimensionStickerService {
|
||||
const pack = stickerPacks[0];
|
||||
|
||||
// Simulate a call to setPackSelected
|
||||
await this.setPackSelected(scalarToken, pack.id, {isSelected: true});
|
||||
await this.setPackSelected(pack.id, {isSelected: true});
|
||||
|
||||
const memoryPack = await DimensionStickerService.packToMemory(pack);
|
||||
return Object.assign({isSelected: true}, memoryPack);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, DELETE, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
|
||||
import { TelegramBridge } from "../../bridges/TelegramBridge";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
interface PortalInfoResponse {
|
||||
bridged: boolean;
|
||||
@ -20,16 +19,16 @@ interface BridgeRoomRequest {
|
||||
* API for interacting with the Telegram bridge
|
||||
*/
|
||||
@Path("/api/v1/dimension/telegram")
|
||||
@AutoWired
|
||||
export class DimensionTelegramService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("chat/:chatId")
|
||||
public async getPortalInfo(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async getPortalInfo(@PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const telegram = new TelegramBridge(userId);
|
||||
@ -51,8 +50,9 @@ export class DimensionTelegramService {
|
||||
|
||||
@POST
|
||||
@Path("chat/:chatId/room/:roomId")
|
||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async bridgeRoom(@PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const telegram = new TelegramBridge(userId);
|
||||
@ -73,8 +73,9 @@ export class DimensionTelegramService {
|
||||
|
||||
@DELETE
|
||||
@Path("room/:roomId")
|
||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
@Security(ROLE_USER)
|
||||
public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
|
||||
try {
|
||||
const telegram = new TelegramBridge(userId);
|
||||
|
@ -1,23 +1,31 @@
|
||||
import { DELETE, FormParam, HeaderParam, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||
import {
|
||||
Context,
|
||||
DELETE,
|
||||
FormParam,
|
||||
HeaderParam,
|
||||
Path,
|
||||
PathParam,
|
||||
POST,
|
||||
Security,
|
||||
ServiceContext
|
||||
} from "typescript-rest";
|
||||
import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks";
|
||||
import { WebhooksBridge } from "../../bridges/WebhooksBridge";
|
||||
import Webhook from "../../db/models/Webhook";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import * as request from "request";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
|
||||
/**
|
||||
* API for interacting with the Webhooks bridge, and for setting up proxies to other
|
||||
* services.
|
||||
*/
|
||||
@Path("/api/v1/dimension/webhooks")
|
||||
@AutoWired
|
||||
export class DimensionWebhooksService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@POST
|
||||
@Path("/travisci/:webhookId")
|
||||
@ -47,27 +55,27 @@ export class DimensionWebhooksService {
|
||||
|
||||
@POST
|
||||
@Path("room/:roomId/webhooks/new")
|
||||
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async newWebhook(@PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const webhooks = new WebhooksBridge(userId);
|
||||
return webhooks.createWebhook(roomId, options);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("room/:roomId/webhooks/:hookId")
|
||||
public async updateWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async updateWebhook(@PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const webhooks = new WebhooksBridge(userId);
|
||||
return webhooks.updateWebhook(roomId, hookId, options);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("room/:roomId/webhooks/:hookId")
|
||||
public async deleteWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async deleteWebhook(@PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
|
||||
const userId = this.context.request.user.userId;
|
||||
const webhooks = new WebhooksBridge(userId);
|
||||
return webhooks.deleteWebhook(roomId, hookId);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GET, Path, QueryParam } from "typescript-rest";
|
||||
import { GET, Path, PathParam, QueryParam } from "typescript-rest";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import * as url from "url";
|
||||
import { ApiError } from "../ApiError";
|
||||
@ -6,17 +6,43 @@ import * as dns from "dns-then";
|
||||
import config from "../../config";
|
||||
import { Netmask } from "netmask";
|
||||
import * as request from "request";
|
||||
import { VERSION_DRAFT } from "../controllers/TermsController";
|
||||
import TermsRecord from "../../db/models/TermsRecord";
|
||||
import TermsTextRecord from "../../db/models/TermsTextRecord";
|
||||
|
||||
interface EmbedCapabilityResponse {
|
||||
canEmbed: boolean;
|
||||
}
|
||||
|
||||
interface MinimalTermsResponse {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API for widgets
|
||||
*/
|
||||
@Path("/api/v1/dimension/widgets")
|
||||
export class DimensionWidgetService {
|
||||
|
||||
@GET
|
||||
@Path("/terms/:shortcode/:language/:version")
|
||||
public async getPolicy(@PathParam("shortcode") shortcode: string, @PathParam("language") language: string, @PathParam("version") version: string): Promise<MinimalTermsResponse> {
|
||||
if (version === VERSION_DRAFT) {
|
||||
throw new ApiError(401, "Cannot access draft versions of policies", "M_UNAUTHORIZED");
|
||||
}
|
||||
|
||||
const terms = await TermsRecord.findOne({where: {shortcode, version}, include: [TermsTextRecord]});
|
||||
if (!terms) throw new ApiError(404, "Not found", "M_NOT_FOUND");
|
||||
|
||||
const text = terms.texts.find(t => t.language === language);
|
||||
|
||||
return {
|
||||
name: text.name,
|
||||
text: text.text,
|
||||
};
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("embeddable")
|
||||
public async isEmbeddable(@QueryParam("url") checkUrl: string): Promise<EmbedCapabilityResponse> {
|
||||
|
@ -2,14 +2,15 @@ import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-r
|
||||
import { OpenId } from "../../models/OpenId";
|
||||
import AccountController, { IAccountInfoResponse, IAccountRegisteredResponse } from "../controllers/AccountController";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { IMSCUser, ROLE_MSC_USER } from "../security/MSCSecurity";
|
||||
import { ILoggedInUser, ROLE_USER } from "../security/MatrixSecurity";
|
||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
|
||||
/**
|
||||
* API for account management
|
||||
*/
|
||||
@Path("/_matrix/integrations/v1/account")
|
||||
@AutoWired
|
||||
export class MSCAccountService {
|
||||
export class MatrixAccountService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
@ -20,20 +21,20 @@ export class MSCAccountService {
|
||||
@POST
|
||||
@Path("register")
|
||||
public async register(request: OpenId): Promise<IAccountRegisteredResponse> {
|
||||
return this.accountController.registerAccount(request);
|
||||
return this.accountController.registerAccount(request, ScalarClient.KIND_MATRIX_V1);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Security(ROLE_MSC_USER)
|
||||
@Security(ROLE_USER)
|
||||
public async info(): Promise<IAccountInfoResponse> {
|
||||
const user: IMSCUser = this.context.request.user;
|
||||
const user: ILoggedInUser = this.context.request.user;
|
||||
return {user_id: user.userId};
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("logout")
|
||||
@Security(ROLE_MSC_USER)
|
||||
@Security(ROLE_USER)
|
||||
public async logout(): Promise<any> {
|
||||
await this.accountController.logout(this.context.request.user);
|
||||
return {};
|
36
src/api/matrix/MatrixTermsService.ts
Normal file
36
src/api/matrix/MatrixTermsService.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
import TermsController, { ITermsResponse } from "../controllers/TermsController";
|
||||
|
||||
export interface SignTermsRequest {
|
||||
user_accepts: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API for account management
|
||||
*/
|
||||
@Path("/_matrix/integrations/v1/terms")
|
||||
@AutoWired
|
||||
export class MatrixTermsService {
|
||||
|
||||
@Inject
|
||||
private termsController: TermsController;
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
public async getAllTerms(): Promise<ITermsResponse> {
|
||||
return this.termsController.getAvailableTerms();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("")
|
||||
@Security(ROLE_USER)
|
||||
public async signTerms(request: SignTermsRequest): Promise<any> {
|
||||
await this.termsController.signTermsMatching(this.context.request.user, request.user_accepts);
|
||||
return {};
|
||||
}
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
||||
import { Context, GET, Path, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { OpenId } from "../../models/OpenId";
|
||||
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
import TermsController, { ITermsResponse } from "../controllers/TermsController";
|
||||
import { SignTermsRequest } from "../matrix/MatrixTermsService";
|
||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
|
||||
/**
|
||||
* API for the minimum Scalar API we need to implement to be compatible with clients. Used for registration
|
||||
@ -16,6 +20,12 @@ export class ScalarService {
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@Inject
|
||||
private termsController: TermsController;
|
||||
|
||||
@Context
|
||||
private context: ServiceContext;
|
||||
|
||||
@POST
|
||||
@Path("register")
|
||||
public async register(request: OpenId, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
|
||||
@ -23,19 +33,33 @@ export class ScalarService {
|
||||
throw new ApiError(401, "Invalid API version.");
|
||||
}
|
||||
|
||||
const response = await this.accountController.registerAccount(request);
|
||||
const response = await this.accountController.registerAccount(request, ScalarClient.KIND_LEGACY);
|
||||
return {scalar_token: response.token};
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("account")
|
||||
public async getAccount(@QueryParam("scalar_token") scalarToken: string, @QueryParam("v") apiVersion: string): Promise<ScalarAccountResponse> {
|
||||
@Security(ROLE_USER)
|
||||
public async getAccount(@QueryParam("v") apiVersion: string): Promise<ScalarAccountResponse> {
|
||||
if (apiVersion !== "1.1") {
|
||||
throw new ApiError(401, "Invalid API version.");
|
||||
}
|
||||
|
||||
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||
return {user_id: userId};
|
||||
return {user_id: this.context.request.user.userId};
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("terms")
|
||||
public async getTerms(): Promise<ITermsResponse> {
|
||||
return this.termsController.getAvailableTerms();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("terms")
|
||||
@Security(ROLE_USER)
|
||||
public async signTerms(request: SignTermsRequest): Promise<any> {
|
||||
await this.termsController.signTermsMatching(this.context.request.user, request.user_accepts);
|
||||
return {};
|
||||
}
|
||||
|
||||
@GET
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { GET, Path, QueryParam } from "typescript-rest";
|
||||
import { GET, Path, QueryParam, Security } from "typescript-rest";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache";
|
||||
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
||||
import config from "../../config";
|
||||
import { ROLE_USER } from "../security/MatrixSecurity";
|
||||
import moment = require("moment");
|
||||
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
interface UrlPreviewResponse {
|
||||
cached_response: boolean;
|
||||
@ -23,17 +22,12 @@ interface UrlPreviewResponse {
|
||||
* API for the minimum Scalar API for widget functionality in clients.
|
||||
*/
|
||||
@Path("/api/v1/scalar/widgets")
|
||||
@AutoWired
|
||||
export class ScalarWidgetService {
|
||||
|
||||
@Inject
|
||||
private accountController: AccountController;
|
||||
|
||||
@GET
|
||||
@Path("title_lookup")
|
||||
public async titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
||||
await this.accountController.getTokenOwner(scalarToken);
|
||||
|
||||
@Security(ROLE_USER)
|
||||
public async titleLookup(@QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
||||
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
|
||||
if (cachedResult) {
|
||||
cachedResult.cached_response = true;
|
||||
|
@ -1,63 +0,0 @@
|
||||
import { ServiceAuthenticator } from "typescript-rest";
|
||||
import { Request, RequestHandler, Response, Router } from "express";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
|
||||
export interface IMSCUser {
|
||||
userId: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const ROLE_MSC_USER = "ROLE_MSC_USER";
|
||||
export const ROLE_MSC_TERMS_SIGNED = "ROLE_MSC_TERMS_SIGNED";
|
||||
|
||||
export default class MSCSecurity implements ServiceAuthenticator {
|
||||
|
||||
private accountController = new AccountController();
|
||||
|
||||
public getRoles(req: Request): string[] {
|
||||
if (req.user) return [ROLE_MSC_USER];
|
||||
return [];
|
||||
}
|
||||
|
||||
getMiddleware(): RequestHandler {
|
||||
return (async (req: Request, res: Response, next: () => void) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
if (req.headers.authorization) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header.startsWith("Bearer ")) {
|
||||
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
|
||||
}
|
||||
token = header.substring("Bearer ".length);
|
||||
} else if (req.query && req.query.access_token) {
|
||||
token = req.query.access_token;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
req.user = <IMSCUser>{
|
||||
userId: await this.accountController.getTokenOwner(token),
|
||||
token: token,
|
||||
};
|
||||
return next();
|
||||
} else {
|
||||
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
// TODO: Proper error message
|
||||
res.status(e.statusCode).json({errcode: e.errorCode, error: "Error"});
|
||||
} else {
|
||||
LogService.error("MSCSecurity", e);
|
||||
res.status(500).json({errcode: "M_UNKNOWN", error: "Unknown server error"});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initialize(_router: Router): void {
|
||||
}
|
||||
|
||||
}
|
133
src/api/security/MatrixSecurity.ts
Normal file
133
src/api/security/MatrixSecurity.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { ServiceAuthenticator } from "typescript-rest";
|
||||
import { Request, RequestHandler, Response, Router } from "express";
|
||||
import { ApiError } from "../ApiError";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import AccountController from "../controllers/AccountController";
|
||||
import TermsController from "../controllers/TermsController";
|
||||
import config from "../../config";
|
||||
import { ScalarStore } from "../../db/ScalarStore";
|
||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
||||
|
||||
export interface ILoggedInUser {
|
||||
userId: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const ROLE_USER = "ROLE_USER";
|
||||
export const ROLE_ADMIN = "ROLE_ADMIN";
|
||||
|
||||
const TERMS_IGNORED_ROUTES = [
|
||||
{method: "*", path: "/api/v1/dimension/admin/"},
|
||||
{method: "GET", path: "/_matrix/integrations/v1/terms"},
|
||||
{method: "POST", path: "/_matrix/integrations/v1/terms"},
|
||||
{method: "POST", path: "/_matrix/integrations/v1/register"},
|
||||
{method: "POST", path: "/_matrix/integrations/v1/logout"},
|
||||
|
||||
// Legacy scalar routes
|
||||
{method: "GET", path: "/api/v1/scalar/terms"},
|
||||
{method: "POST", path: "/api/v1/scalar/terms"},
|
||||
{method: "POST", path: "/api/v1/scalar/register"},
|
||||
];
|
||||
|
||||
const ADMIN_ROUTES = [
|
||||
{method: "*", path: "/api/v1/dimension/admin/"},
|
||||
];
|
||||
|
||||
export default class MatrixSecurity implements ServiceAuthenticator {
|
||||
|
||||
private accountController = new AccountController();
|
||||
private termsController = new TermsController();
|
||||
|
||||
public getRoles(req: Request): string[] {
|
||||
if (req.user) {
|
||||
const roles = [ROLE_USER];
|
||||
if (config.admins.includes(req.user.userId)) {
|
||||
roles.push(ROLE_ADMIN);
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public getMiddleware(): RequestHandler {
|
||||
return (async (req: Request, res: Response, next: () => void) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
if (req.headers.authorization) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header.startsWith("Bearer ")) {
|
||||
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
|
||||
}
|
||||
token = header.substring("Bearer ".length);
|
||||
} else if (req.query && req.query.access_token) {
|
||||
token = req.query.access_token;
|
||||
} else if (req.query && req.query.scalar_token) {
|
||||
LogService.warn("MatrixSecurity", "Request used old scalar_token auth - this will be removed in a future version");
|
||||
token = req.query.scalar_token;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
req.user = <ILoggedInUser>{
|
||||
userId: await this.accountController.getTokenOwner(token),
|
||||
token: token,
|
||||
};
|
||||
|
||||
const needUpstreams = !this.matchesAnyRoute(req, ADMIN_ROUTES);
|
||||
if (needUpstreams) {
|
||||
const scalarKind = req.path.startsWith("/_matrix/integrations/v1/") ? ScalarClient.KIND_MATRIX_V1 : ScalarClient.KIND_LEGACY;
|
||||
const hasUpstreams = await ScalarStore.doesUserHaveTokensForAllUpstreams(req.user.userId, scalarKind);
|
||||
if (!hasUpstreams) {
|
||||
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
|
||||
}
|
||||
}
|
||||
|
||||
const needTerms = !this.matchesAnyRoute(req, TERMS_IGNORED_ROUTES);
|
||||
if (needTerms) {
|
||||
const signatureNeeded = await this.termsController.doesUserNeedToSignTerms(req.user);
|
||||
if (signatureNeeded) {
|
||||
return res.status(403).json({
|
||||
errcode: "M_TERMS_NOT_SIGNED",
|
||||
error: "The user has not accepted all terms of service for this integration manager",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.matchesAnyRoute(req, ADMIN_ROUTES, false) && !this.getRoles(req).includes(ROLE_ADMIN)) {
|
||||
return res.status(403).json({errcode: "M_UNAUTHORIZED", error: "User is not an admin"});
|
||||
}
|
||||
|
||||
return next();
|
||||
} else {
|
||||
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
res.status(e.statusCode).json(e.jsonResponse);
|
||||
} else {
|
||||
LogService.error("MatrixSecurity", e);
|
||||
res.status(500).json({errcode: "M_UNKNOWN", error: "Unknown server error"});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public initialize(_router: Router): void {
|
||||
}
|
||||
|
||||
private matchesAnyRoute(req: Request, routes: { method: string, path: string }[], valForOptions = true): boolean {
|
||||
if (req.method === 'OPTIONS') return valForOptions;
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.method === '*' && req.path.startsWith(route.path)) {
|
||||
return true;
|
||||
}
|
||||
if (route.method === req.method && route.path === req.path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -26,6 +26,10 @@ import WebhookBridgeRecord from "./models/WebhookBridgeRecord";
|
||||
import GitterBridgeRecord from "./models/GitterBridgeRecord";
|
||||
import CustomSimpleBotRecord from "./models/CustomSimpleBotRecord";
|
||||
import SlackBridgeRecord from "./models/SlackBridgeRecord";
|
||||
import TermsRecord from "./models/TermsRecord";
|
||||
import TermsTextRecord from "./models/TermsTextRecord";
|
||||
import TermsSignedRecord from "./models/TermsSignedRecord";
|
||||
import TermsUpstreamRecord from "./models/TermsUpstreamRecord";
|
||||
|
||||
class _DimensionStore {
|
||||
private sequelize: Sequelize;
|
||||
@ -63,6 +67,10 @@ class _DimensionStore {
|
||||
GitterBridgeRecord,
|
||||
CustomSimpleBotRecord,
|
||||
SlackBridgeRecord,
|
||||
TermsRecord,
|
||||
TermsTextRecord,
|
||||
TermsSignedRecord,
|
||||
TermsUpstreamRecord,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,12 @@ import Upstream from "./models/Upstream";
|
||||
import User from "./models/User";
|
||||
import { MatrixStickerBot } from "../matrix/MatrixStickerBot";
|
||||
import { ScalarClient } from "../scalar/ScalarClient";
|
||||
import { CACHE_SCALAR_ONLINE_STATE, Cache } from "../MemoryCache";
|
||||
import { Cache, CACHE_SCALAR_ONLINE_STATE } from "../MemoryCache";
|
||||
import { ILanguagePolicy } from "../api/controllers/TermsController";
|
||||
|
||||
export class ScalarStore {
|
||||
|
||||
public static async doesUserHaveTokensForAllUpstreams(userId: string): Promise<boolean> {
|
||||
public static async doesUserHaveTokensForAllUpstreams(userId: string, scalarKind: string): Promise<boolean> {
|
||||
const scalarTokens = await UserScalarToken.findAll({where: {userId: userId}});
|
||||
const upstreamTokenIds = scalarTokens.filter(t => !t.isDimensionToken).map(t => t.upstreamId);
|
||||
const hasDimensionToken = scalarTokens.filter(t => t.isDimensionToken).length >= 1;
|
||||
@ -20,7 +21,7 @@ export class ScalarStore {
|
||||
|
||||
const upstreams = await Upstream.findAll();
|
||||
for (const upstream of upstreams) {
|
||||
if (!await ScalarStore.isUpstreamOnline(upstream)) {
|
||||
if (!await ScalarStore.isUpstreamOnline(upstream, scalarKind)) {
|
||||
LogService.warn("ScalarStore", `Upstream ${upstream.apiUrl} is offline - assuming token is valid`);
|
||||
continue;
|
||||
}
|
||||
@ -33,24 +34,17 @@ export class ScalarStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<User> {
|
||||
public static async getTokenOwner(scalarToken: string): Promise<User> {
|
||||
const tokens = await UserScalarToken.findAll({
|
||||
where: {isDimensionToken: true, scalarToken: scalarToken},
|
||||
include: [User]
|
||||
});
|
||||
if (!tokens || tokens.length === 0) throw new Error("Invalid token");
|
||||
|
||||
const user = tokens[0].user;
|
||||
if (ignoreUpstreams) return user; // skip upstreams check
|
||||
|
||||
const hasAllTokens = await ScalarStore.doesUserHaveTokensForAllUpstreams(user.userId);
|
||||
if (!hasAllTokens) {
|
||||
throw new Error("Invalid token"); // They are missing an upstream, so we'll lie and say they are not authorized
|
||||
}
|
||||
return user;
|
||||
return tokens[0].user;
|
||||
}
|
||||
|
||||
public static async isUpstreamOnline(upstream: Upstream): Promise<boolean> {
|
||||
public static async isUpstreamOnline(upstream: Upstream, scalarKind: string): Promise<boolean> {
|
||||
const cache = Cache.for(CACHE_SCALAR_ONLINE_STATE);
|
||||
const cacheKey = `Upstream ${upstream.id}`;
|
||||
const result = cache.get(cacheKey);
|
||||
@ -58,17 +52,17 @@ export class ScalarStore {
|
||||
return result;
|
||||
}
|
||||
|
||||
const state = ScalarStore.checkIfUpstreamOnline(upstream);
|
||||
const state = ScalarStore.checkIfUpstreamOnline(upstream, scalarKind);
|
||||
cache.put(cacheKey, state, 60 * 60 * 1000); // 1 hour
|
||||
return state;
|
||||
}
|
||||
|
||||
private static async checkIfUpstreamOnline(upstream: Upstream): Promise<boolean> {
|
||||
private static async checkIfUpstreamOnline(upstream: Upstream, scalarKind: string, signTerms = true): Promise<boolean> {
|
||||
try {
|
||||
// The sticker bot can be used for this for now
|
||||
|
||||
const testUserId = await MatrixStickerBot.getUserId();
|
||||
const scalarClient = new ScalarClient(upstream);
|
||||
const scalarClient = new ScalarClient(upstream, scalarKind);
|
||||
|
||||
// First see if we have a token for the upstream so we can try it
|
||||
const existingTokens = await UserScalarToken.findAll({
|
||||
@ -85,8 +79,14 @@ export class ScalarStore {
|
||||
return true; // it's online
|
||||
} catch (e) {
|
||||
LogService.error("ScalarStore", e);
|
||||
if (!isNaN(Number(e))) {
|
||||
if (e === 401 || e === 403) {
|
||||
if (e && !isNaN(Number(e.statusCode))) {
|
||||
if (e.statusCode === 403 && e.body) {
|
||||
if (e.body.errcode === 'M_TERMS_NOT_SIGNED' && signTerms) {
|
||||
await ScalarStore.signAllTerms(existingTokens[0], scalarKind);
|
||||
return ScalarStore.checkIfUpstreamOnline(upstream, scalarKind, false);
|
||||
}
|
||||
}
|
||||
if (e.statusCode === 401 || e.statusCode === 403) {
|
||||
LogService.info("ScalarStore", "Test user token expired");
|
||||
} else {
|
||||
// Assume offline
|
||||
@ -114,13 +114,16 @@ export class ScalarStore {
|
||||
|
||||
const openId = await MatrixStickerBot.getOpenId();
|
||||
const token = await scalarClient.register(openId);
|
||||
await UserScalarToken.create({
|
||||
const scalarToken = await UserScalarToken.create({
|
||||
userId: testUserId,
|
||||
scalarToken: token.scalar_token,
|
||||
isDimensionToken: false,
|
||||
upstreamId: upstream.id,
|
||||
});
|
||||
|
||||
// Accept all terms of service for the user
|
||||
await ScalarStore.signAllTerms(scalarToken, scalarKind);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
LogService.error("ScalarStore", e);
|
||||
@ -128,6 +131,21 @@ export class ScalarStore {
|
||||
}
|
||||
}
|
||||
|
||||
private static async signAllTerms(token: UserScalarToken, scalarKind: string) {
|
||||
try {
|
||||
const client = new ScalarClient(token.upstream, scalarKind);
|
||||
const terms = await client.getAvailableTerms();
|
||||
const urlsToSign = Object.values(terms.policies).map(p => {
|
||||
const englishCode = Object.keys(p).find(k => k.toLowerCase() === 'en' || k.toLowerCase().startsWith('en_'));
|
||||
if (!englishCode) return null;
|
||||
return (<ILanguagePolicy>p[englishCode]).url;
|
||||
}).filter(v => !!v);
|
||||
await client.signTermsUrls(token.scalarToken, urlsToSign);
|
||||
} catch (e) {
|
||||
LogService.error("ScalarStore", e);
|
||||
}
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
}
|
@ -78,44 +78,368 @@ export default {
|
||||
},
|
||||
]))
|
||||
.then(() => queryInterface.bulkInsert("dimension_stickers", [
|
||||
{ packId: 1, name: "Happy", description: "A very happy husky", imageMxc: "mxc://t2bot.io/b4636e93388542f3cad8fcbb825adf36", thumbnailMxc: "mxc://t2bot.io/920b7a90c8d66f0de0bc2e4e7f1b3d90", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 1, name: "Laughing", description: "A husky laughs at something presumably funny", imageMxc: "mxc://t2bot.io/12e39b87ce8099ff5951b9c37405bac5", thumbnailMxc: "mxc://t2bot.io/cf2a8f9179bdb59ec3dfdc15a71eb12d", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 1, name: "Sad", description: "This husky is sad :(", imageMxc: "mxc://t2bot.io/96777697c144918fe80fca92c90e3208", thumbnailMxc: "mxc://t2bot.io/afbaad67303a70da9e5af39c6d55ab7e", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 1, name: "Heart Eyes", description: "This husky loves what he sees", imageMxc: "mxc://t2bot.io/193408b58f5e1eb72d9bea13f23914e6", thumbnailMxc: "mxc://t2bot.io/27038b035508f695eb0bfa99cc567cd9", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 1, name: "Sleeping", description: "Zzz", imageMxc: "mxc://t2bot.io/e3b518c8aa9f91efd7aaa2195a4662b0", thumbnailMxc: "mxc://t2bot.io/c7c89ac2f8edba4ca9fbe50a547c3d0c", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 1, name: "Dancing", description: "Either this husky is starting a dance party, or one is underway", imageMxc: "mxc://t2bot.io/f96cc6fcc48ec85dd9a160be18fa30c0", thumbnailMxc: "mxc://t2bot.io/a548fe46c6a979bd7de5fe1fee7b35b0", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{
|
||||
packId: 1,
|
||||
name: "Happy",
|
||||
description: "A very happy husky",
|
||||
imageMxc: "mxc://t2bot.io/b4636e93388542f3cad8fcbb825adf36",
|
||||
thumbnailMxc: "mxc://t2bot.io/920b7a90c8d66f0de0bc2e4e7f1b3d90",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 1,
|
||||
name: "Laughing",
|
||||
description: "A husky laughs at something presumably funny",
|
||||
imageMxc: "mxc://t2bot.io/12e39b87ce8099ff5951b9c37405bac5",
|
||||
thumbnailMxc: "mxc://t2bot.io/cf2a8f9179bdb59ec3dfdc15a71eb12d",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 1,
|
||||
name: "Sad",
|
||||
description: "This husky is sad :(",
|
||||
imageMxc: "mxc://t2bot.io/96777697c144918fe80fca92c90e3208",
|
||||
thumbnailMxc: "mxc://t2bot.io/afbaad67303a70da9e5af39c6d55ab7e",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 1,
|
||||
name: "Heart Eyes",
|
||||
description: "This husky loves what he sees",
|
||||
imageMxc: "mxc://t2bot.io/193408b58f5e1eb72d9bea13f23914e6",
|
||||
thumbnailMxc: "mxc://t2bot.io/27038b035508f695eb0bfa99cc567cd9",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 1,
|
||||
name: "Sleeping",
|
||||
description: "Zzz",
|
||||
imageMxc: "mxc://t2bot.io/e3b518c8aa9f91efd7aaa2195a4662b0",
|
||||
thumbnailMxc: "mxc://t2bot.io/c7c89ac2f8edba4ca9fbe50a547c3d0c",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 1,
|
||||
name: "Dancing",
|
||||
description: "Either this husky is starting a dance party, or one is underway",
|
||||
imageMxc: "mxc://t2bot.io/f96cc6fcc48ec85dd9a160be18fa30c0",
|
||||
thumbnailMxc: "mxc://t2bot.io/a548fe46c6a979bd7de5fe1fee7b35b0",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
|
||||
{ packId: 2, name: "Winking", description: "A winking cat", imageMxc: "mxc://t2bot.io/ccecfeed9e27d1180865ae27a08e2b7a", thumbnailMxc: "mxc://t2bot.io/ccecfeed9e27d1180865ae27a08e2b7a", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Heart Eyes", description: "This cat loves what it sees", imageMxc: "mxc://t2bot.io/0fc5a6aab879b64243f6018167b54216", thumbnailMxc: "mxc://t2bot.io/0fc5a6aab879b64243f6018167b54216", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Brown and Pink", description: "A cute brown and pink cat", imageMxc: "mxc://t2bot.io/ebcf532e183df1e8c7d983af2bbcfffc", thumbnailMxc: "mxc://t2bot.io/ebcf532e183df1e8c7d983af2bbcfffc", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Blushing", description: "Something embarrassing must have happened", imageMxc: "mxc://t2bot.io/017516e6a96b5bb7b9b8bb9302c51548", thumbnailMxc: "mxc://t2bot.io/017516e6a96b5bb7b9b8bb9302c51548", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Angry", description: "This cat is not happy", imageMxc: "mxc://t2bot.io/33761e2e7c39bc347c128b02715d28fd", thumbnailMxc: "mxc://t2bot.io/33761e2e7c39bc347c128b02715d28fd", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Disappointed", description: "This cat is disappointed", imageMxc: "mxc://t2bot.io/fae848ca3651131cc5b15bda728fb048", thumbnailMxc: "mxc://t2bot.io/fae848ca3651131cc5b15bda728fb048", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Very Happy", description: "A happy cat is a good cat!", imageMxc: "mxc://t2bot.io/fc06e95e9d2e3c62d5c577fbc6186f25", thumbnailMxc: "mxc://t2bot.io/fc06e95e9d2e3c62d5c577fbc6186f25", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Sad", description: "A sad cat :(", imageMxc: "mxc://t2bot.io/8be3ef58dc89fc269d37e5c978b37c2d", thumbnailMxc: "mxc://t2bot.io/8be3ef58dc89fc269d37e5c978b37c2d", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Happy", description: "A happy cat", imageMxc: "mxc://t2bot.io/2d7be360df8fa679e8a24b89b9b32aad", thumbnailMxc: "mxc://t2bot.io/2d7be360df8fa679e8a24b89b9b32aad", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Nope", description: "The cat has turned away, presumably to express disapproval", imageMxc: "mxc://t2bot.io/fb9cef3190b643c53c61538701763d36", thumbnailMxc: "mxc://t2bot.io/fb9cef3190b643c53c61538701763d36", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Shocked", description: "WHAT HAPPENED!?", imageMxc: "mxc://t2bot.io/45f0bcabd0edcf43801623a8f5675628", thumbnailMxc: "mxc://t2bot.io/45f0bcabd0edcf43801623a8f5675628", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Blue Eyed Wink", description: "A cute blue-eyed cat is winking", imageMxc: "mxc://t2bot.io/1e2c36ce0d191c47e74f60b45c377f3d", thumbnailMxc: "mxc://t2bot.io/1e2c36ce0d191c47e74f60b45c377f3d", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Sad", description: "A sad cat :(", imageMxc: "mxc://t2bot.io/f2d2c5490097d6542e96d295e415cb6f", thumbnailMxc: "mxc://t2bot.io/f2d2c5490097d6542e96d295e415cb6f", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Disappointed", description: "This cat is disappointed", imageMxc: "mxc://t2bot.io/00f8d2832087a4cbcd98f090864f3357", thumbnailMxc: "mxc://t2bot.io/00f8d2832087a4cbcd98f090864f3357", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 2, name: "Happy", description: "A happy cat", imageMxc: "mxc://t2bot.io/8c88a05eb8e5a555830c8fffa36043f5", thumbnailMxc: "mxc://t2bot.io/8c88a05eb8e5a555830c8fffa36043f5", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{
|
||||
packId: 2,
|
||||
name: "Winking",
|
||||
description: "A winking cat",
|
||||
imageMxc: "mxc://t2bot.io/ccecfeed9e27d1180865ae27a08e2b7a",
|
||||
thumbnailMxc: "mxc://t2bot.io/ccecfeed9e27d1180865ae27a08e2b7a",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Heart Eyes",
|
||||
description: "This cat loves what it sees",
|
||||
imageMxc: "mxc://t2bot.io/0fc5a6aab879b64243f6018167b54216",
|
||||
thumbnailMxc: "mxc://t2bot.io/0fc5a6aab879b64243f6018167b54216",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Brown and Pink",
|
||||
description: "A cute brown and pink cat",
|
||||
imageMxc: "mxc://t2bot.io/ebcf532e183df1e8c7d983af2bbcfffc",
|
||||
thumbnailMxc: "mxc://t2bot.io/ebcf532e183df1e8c7d983af2bbcfffc",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Blushing",
|
||||
description: "Something embarrassing must have happened",
|
||||
imageMxc: "mxc://t2bot.io/017516e6a96b5bb7b9b8bb9302c51548",
|
||||
thumbnailMxc: "mxc://t2bot.io/017516e6a96b5bb7b9b8bb9302c51548",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Angry",
|
||||
description: "This cat is not happy",
|
||||
imageMxc: "mxc://t2bot.io/33761e2e7c39bc347c128b02715d28fd",
|
||||
thumbnailMxc: "mxc://t2bot.io/33761e2e7c39bc347c128b02715d28fd",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Disappointed",
|
||||
description: "This cat is disappointed",
|
||||
imageMxc: "mxc://t2bot.io/fae848ca3651131cc5b15bda728fb048",
|
||||
thumbnailMxc: "mxc://t2bot.io/fae848ca3651131cc5b15bda728fb048",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Very Happy",
|
||||
description: "A happy cat is a good cat!",
|
||||
imageMxc: "mxc://t2bot.io/fc06e95e9d2e3c62d5c577fbc6186f25",
|
||||
thumbnailMxc: "mxc://t2bot.io/fc06e95e9d2e3c62d5c577fbc6186f25",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Sad",
|
||||
description: "A sad cat :(",
|
||||
imageMxc: "mxc://t2bot.io/8be3ef58dc89fc269d37e5c978b37c2d",
|
||||
thumbnailMxc: "mxc://t2bot.io/8be3ef58dc89fc269d37e5c978b37c2d",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Happy",
|
||||
description: "A happy cat",
|
||||
imageMxc: "mxc://t2bot.io/2d7be360df8fa679e8a24b89b9b32aad",
|
||||
thumbnailMxc: "mxc://t2bot.io/2d7be360df8fa679e8a24b89b9b32aad",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Nope",
|
||||
description: "The cat has turned away, presumably to express disapproval",
|
||||
imageMxc: "mxc://t2bot.io/fb9cef3190b643c53c61538701763d36",
|
||||
thumbnailMxc: "mxc://t2bot.io/fb9cef3190b643c53c61538701763d36",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Shocked",
|
||||
description: "WHAT HAPPENED!?",
|
||||
imageMxc: "mxc://t2bot.io/45f0bcabd0edcf43801623a8f5675628",
|
||||
thumbnailMxc: "mxc://t2bot.io/45f0bcabd0edcf43801623a8f5675628",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Blue Eyed Wink",
|
||||
description: "A cute blue-eyed cat is winking",
|
||||
imageMxc: "mxc://t2bot.io/1e2c36ce0d191c47e74f60b45c377f3d",
|
||||
thumbnailMxc: "mxc://t2bot.io/1e2c36ce0d191c47e74f60b45c377f3d",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Sad",
|
||||
description: "A sad cat :(",
|
||||
imageMxc: "mxc://t2bot.io/f2d2c5490097d6542e96d295e415cb6f",
|
||||
thumbnailMxc: "mxc://t2bot.io/f2d2c5490097d6542e96d295e415cb6f",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Disappointed",
|
||||
description: "This cat is disappointed",
|
||||
imageMxc: "mxc://t2bot.io/00f8d2832087a4cbcd98f090864f3357",
|
||||
thumbnailMxc: "mxc://t2bot.io/00f8d2832087a4cbcd98f090864f3357",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 2,
|
||||
name: "Happy",
|
||||
description: "A happy cat",
|
||||
imageMxc: "mxc://t2bot.io/8c88a05eb8e5a555830c8fffa36043f5",
|
||||
thumbnailMxc: "mxc://t2bot.io/8c88a05eb8e5a555830c8fffa36043f5",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
|
||||
{ packId: 3, name: "Winking", description: "A winking cat", imageMxc: "mxc://t2bot.io/7a947ba45c90a96e35edfd8873acb9e6", thumbnailMxc: "mxc://t2bot.io/7a947ba45c90a96e35edfd8873acb9e6", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Heart Eyes", description: "This cat loves what it sees", imageMxc: "mxc://t2bot.io/a60555fd09e42be02119a4a75db993e7", thumbnailMxc: "mxc://t2bot.io/a60555fd09e42be02119a4a75db993e7", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Brown and Pink", description: "A cute brown and pink cat", imageMxc: "mxc://t2bot.io/936279aba6da2672a92005df5218a4be", thumbnailMxc: "mxc://t2bot.io/936279aba6da2672a92005df5218a4be", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Blushing", description: "Something embarrassing must have happened", imageMxc: "mxc://t2bot.io/63cac43a98ed00e294fdf741688cb495", thumbnailMxc: "mxc://t2bot.io/63cac43a98ed00e294fdf741688cb495", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Angry", description: "This cat is not happy", imageMxc: "mxc://t2bot.io/25b956c807a97cf9530abd29b886159b", thumbnailMxc: "mxc://t2bot.io/25b956c807a97cf9530abd29b886159b", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Disappointed", description: "This cat is disappointed", imageMxc: "mxc://t2bot.io/079440f60dd90b363773cd544422c19f", thumbnailMxc: "mxc://t2bot.io/079440f60dd90b363773cd544422c19f", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Very Happy", description: "A happy cat is a good cat!", imageMxc: "mxc://t2bot.io/cb803dbff4fb2cbf7b1306a4a1baf81d", thumbnailMxc: "mxc://t2bot.io/cb803dbff4fb2cbf7b1306a4a1baf81d", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Sad", description: "A sad cat :(", imageMxc: "mxc://t2bot.io/de5e985f346eecd4eb43eb942303759a", thumbnailMxc: "mxc://t2bot.io/de5e985f346eecd4eb43eb942303759a", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Happy", description: "A happy cat", imageMxc: "mxc://t2bot.io/01e06e2489185ac5b1fc73c904e1d5f0", thumbnailMxc: "mxc://t2bot.io/01e06e2489185ac5b1fc73c904e1d5f0", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Nope", description: "The cat has turned away, presumably to express disapproval", imageMxc: "mxc://t2bot.io/022d86b55f623505667e5fb5fcc49cff", thumbnailMxc: "mxc://t2bot.io/022d86b55f623505667e5fb5fcc49cff", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Shocked", description: "WHAT HAPPENED!?", imageMxc: "mxc://t2bot.io/1413113b209a869ec42a52fbc0a8fa49", thumbnailMxc: "mxc://t2bot.io/1413113b209a869ec42a52fbc0a8fa49", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Blue Eyed Wink", description: "A cute blue-eyed cat is winking", imageMxc: "mxc://t2bot.io/9beee7587cb9db56e6fabf01b0a8d168", thumbnailMxc: "mxc://t2bot.io/9beee7587cb9db56e6fabf01b0a8d168", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Sad", description: "A sad cat :(", imageMxc: "mxc://t2bot.io/3bbf2d94865298661af30816ce4a7a75", thumbnailMxc: "mxc://t2bot.io/3bbf2d94865298661af30816ce4a7a75", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Disappointed", description: "This cat is disappointed", imageMxc: "mxc://t2bot.io/45a1b772d51ddbc374768940b4a80f3c", thumbnailMxc: "mxc://t2bot.io/45a1b772d51ddbc374768940b4a80f3c", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: 3, name: "Happy", description: "A happy cat", imageMxc: "mxc://t2bot.io/106b63dbb114ca121d09765971a8b093", thumbnailMxc: "mxc://t2bot.io/106b63dbb114ca121d09765971a8b093", mimetype: "image/png", thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{
|
||||
packId: 3,
|
||||
name: "Winking",
|
||||
description: "A winking cat",
|
||||
imageMxc: "mxc://t2bot.io/7a947ba45c90a96e35edfd8873acb9e6",
|
||||
thumbnailMxc: "mxc://t2bot.io/7a947ba45c90a96e35edfd8873acb9e6",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Heart Eyes",
|
||||
description: "This cat loves what it sees",
|
||||
imageMxc: "mxc://t2bot.io/a60555fd09e42be02119a4a75db993e7",
|
||||
thumbnailMxc: "mxc://t2bot.io/a60555fd09e42be02119a4a75db993e7",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Brown and Pink",
|
||||
description: "A cute brown and pink cat",
|
||||
imageMxc: "mxc://t2bot.io/936279aba6da2672a92005df5218a4be",
|
||||
thumbnailMxc: "mxc://t2bot.io/936279aba6da2672a92005df5218a4be",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Blushing",
|
||||
description: "Something embarrassing must have happened",
|
||||
imageMxc: "mxc://t2bot.io/63cac43a98ed00e294fdf741688cb495",
|
||||
thumbnailMxc: "mxc://t2bot.io/63cac43a98ed00e294fdf741688cb495",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Angry",
|
||||
description: "This cat is not happy",
|
||||
imageMxc: "mxc://t2bot.io/25b956c807a97cf9530abd29b886159b",
|
||||
thumbnailMxc: "mxc://t2bot.io/25b956c807a97cf9530abd29b886159b",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Disappointed",
|
||||
description: "This cat is disappointed",
|
||||
imageMxc: "mxc://t2bot.io/079440f60dd90b363773cd544422c19f",
|
||||
thumbnailMxc: "mxc://t2bot.io/079440f60dd90b363773cd544422c19f",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Very Happy",
|
||||
description: "A happy cat is a good cat!",
|
||||
imageMxc: "mxc://t2bot.io/cb803dbff4fb2cbf7b1306a4a1baf81d",
|
||||
thumbnailMxc: "mxc://t2bot.io/cb803dbff4fb2cbf7b1306a4a1baf81d",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Sad",
|
||||
description: "A sad cat :(",
|
||||
imageMxc: "mxc://t2bot.io/de5e985f346eecd4eb43eb942303759a",
|
||||
thumbnailMxc: "mxc://t2bot.io/de5e985f346eecd4eb43eb942303759a",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Happy",
|
||||
description: "A happy cat",
|
||||
imageMxc: "mxc://t2bot.io/01e06e2489185ac5b1fc73c904e1d5f0",
|
||||
thumbnailMxc: "mxc://t2bot.io/01e06e2489185ac5b1fc73c904e1d5f0",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Nope",
|
||||
description: "The cat has turned away, presumably to express disapproval",
|
||||
imageMxc: "mxc://t2bot.io/022d86b55f623505667e5fb5fcc49cff",
|
||||
thumbnailMxc: "mxc://t2bot.io/022d86b55f623505667e5fb5fcc49cff",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Shocked",
|
||||
description: "WHAT HAPPENED!?",
|
||||
imageMxc: "mxc://t2bot.io/1413113b209a869ec42a52fbc0a8fa49",
|
||||
thumbnailMxc: "mxc://t2bot.io/1413113b209a869ec42a52fbc0a8fa49",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Blue Eyed Wink",
|
||||
description: "A cute blue-eyed cat is winking",
|
||||
imageMxc: "mxc://t2bot.io/9beee7587cb9db56e6fabf01b0a8d168",
|
||||
thumbnailMxc: "mxc://t2bot.io/9beee7587cb9db56e6fabf01b0a8d168",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Sad",
|
||||
description: "A sad cat :(",
|
||||
imageMxc: "mxc://t2bot.io/3bbf2d94865298661af30816ce4a7a75",
|
||||
thumbnailMxc: "mxc://t2bot.io/3bbf2d94865298661af30816ce4a7a75",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Disappointed",
|
||||
description: "This cat is disappointed",
|
||||
imageMxc: "mxc://t2bot.io/45a1b772d51ddbc374768940b4a80f3c",
|
||||
thumbnailMxc: "mxc://t2bot.io/45a1b772d51ddbc374768940b4a80f3c",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: 3,
|
||||
name: "Happy",
|
||||
description: "A happy cat",
|
||||
imageMxc: "mxc://t2bot.io/106b63dbb114ca121d09765971a8b093",
|
||||
thumbnailMxc: "mxc://t2bot.io/106b63dbb114ca121d09765971a8b093",
|
||||
mimetype: "image/png",
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
]));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
|
@ -20,48 +20,426 @@ export default {
|
||||
]))
|
||||
.then(packId => {
|
||||
return queryInterface.bulkInsert("dimension_stickers", [
|
||||
{ packId: packId, name: 'Argue', description: 'Two people arguing in a heated debate', imageMxc: 'mxc://t2bot.io/cfe97ad50ee1f35de322306f58d9d4a1', thumbnailMxc: 'mxc://t2bot.io/cfe97ad50ee1f35de322306f58d9d4a1', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Awake', description: 'Lying awake at night', imageMxc: 'mxc://t2bot.io/0046a75ce93d1322cf9577ea11a49f04', thumbnailMxc: 'mxc://t2bot.io/0046a75ce93d1322cf9577ea11a49f04', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Bed', description: 'Crying yourself to sleep', imageMxc: 'mxc://t2bot.io/fadda48f250674af3f7e3bee55c91b80', thumbnailMxc: 'mxc://t2bot.io/fadda48f250674af3f7e3bee55c91b80', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Bird', description: 'Angry bird', imageMxc: 'mxc://t2bot.io/8a8603d8d6c375e0e92171fc24e37e49', thumbnailMxc: 'mxc://t2bot.io/35eb7a9e416bc5634fac5c9f0f4b0ced', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Blush', description: 'Blushing and looking away', imageMxc: 'mxc://t2bot.io/535d46b7834885bf13bd9ff833561ce5', thumbnailMxc: 'mxc://t2bot.io/535d46b7834885bf13bd9ff833561ce5', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Celebrate', description: 'Opening up a bottle of champagne', imageMxc: 'mxc://t2bot.io/1377f37c0b6a5c401483c4be99e48938', thumbnailMxc: 'mxc://t2bot.io/1377f37c0b6a5c401483c4be99e48938', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Cold', description: 'Shivering in the cold', imageMxc: 'mxc://t2bot.io/3ba28460aae70d33dd061b3a000f5ee2', thumbnailMxc: 'mxc://t2bot.io/3ba28460aae70d33dd061b3a000f5ee2', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Dead Inside', description: 'I feel dead inside', imageMxc: 'mxc://t2bot.io/15196e0235422de488f2d3909054ab82', thumbnailMxc: 'mxc://t2bot.io/15196e0235422de488f2d3909054ab82', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Detective', description: 'A pondering detective', imageMxc: 'mxc://t2bot.io/5042e46fa9d514694c6cbd0a8a1b23b5', thumbnailMxc: 'mxc://t2bot.io/5042e46fa9d514694c6cbd0a8a1b23b5', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Distraught', description: 'Screaming on my knees', imageMxc: 'mxc://t2bot.io/a4173bc5a9ea16528c8442e800513597', thumbnailMxc: 'mxc://t2bot.io/a4173bc5a9ea16528c8442e800513597', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Donut', description: 'Eating a donut', imageMxc: 'mxc://t2bot.io/e3ca7aaa7318c141fd8d2f0a7cd2c52e', thumbnailMxc: 'mxc://t2bot.io/e3ca7aaa7318c141fd8d2f0a7cd2c52e', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Drawing', description: 'Drawing on a sketchbook', imageMxc: 'mxc://t2bot.io/a6327bc46b23684398ccbfcbedd78c66', thumbnailMxc: 'mxc://t2bot.io/a6327bc46b23684398ccbfcbedd78c66', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Driving', description: 'Angrily driving', imageMxc: 'mxc://t2bot.io/3275e023a6683075a58eea3d991e4673', thumbnailMxc: 'mxc://t2bot.io/3275e023a6683075a58eea3d991e4673', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Excellent', description: 'Pleased with the results', imageMxc: 'mxc://t2bot.io/311ae79d33cdde34ca4241d40a0e6aaf', thumbnailMxc: 'mxc://t2bot.io/311ae79d33cdde34ca4241d40a0e6aaf', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Flower', description: 'Smells like bees', imageMxc: 'mxc://t2bot.io/b6ac9eb8e5377b54f61cd8de9b36edc7', thumbnailMxc: 'mxc://t2bot.io/b6ac9eb8e5377b54f61cd8de9b36edc7', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Gaming', description: 'Playing a game', imageMxc: 'mxc://t2bot.io/bfd7e0fe2ef7af961cb93ae28c0fda69', thumbnailMxc: 'mxc://t2bot.io/bfd7e0fe2ef7af961cb93ae28c0fda69', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Get Outta Here', description: 'Waving it off with a laugh', imageMxc: 'mxc://t2bot.io/a77772dde35fc7a67a782dfc0e8e9ed9', thumbnailMxc: 'mxc://t2bot.io/a77772dde35fc7a67a782dfc0e8e9ed9', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Heartbroken', description: 'My heart has broken', imageMxc: 'mxc://t2bot.io/d36a5f1255c5f34d5e2e5985d0c38a68', thumbnailMxc: 'mxc://t2bot.io/d36a5f1255c5f34d5e2e5985d0c38a68', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Hi', description: 'A friendly wave', imageMxc: 'mxc://t2bot.io/9b74aaaec56ebb8b4d483fc1840827be', thumbnailMxc: 'mxc://t2bot.io/9b74aaaec56ebb8b4d483fc1840827be', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Hide', description: 'Nervously tries to hide away and look discreet', imageMxc: 'mxc://t2bot.io/032d8648065f36a2155654b3d59ab8bf', thumbnailMxc: 'mxc://t2bot.io/032d8648065f36a2155654b3d59ab8bf', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'J', description: 'Mouth agape', imageMxc: 'mxc://t2bot.io/c27889f840a49eb42781ffa1fe692b0d', thumbnailMxc: 'mxc://t2bot.io/c27889f840a49eb42781ffa1fe692b0d', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Keyboard cat', description: 'A cat sits at a keybaord with a confused look', imageMxc: 'mxc://t2bot.io/7d79434b361aa00f9d7de99a4722cc9f', thumbnailMxc: 'mxc://t2bot.io/7d79434b361aa00f9d7de99a4722cc9f', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Lewd', description: 'Blushingly overwhelmed', imageMxc: 'mxc://t2bot.io/3076746d3bda4a0bd581bf1bcd3f04de', thumbnailMxc: 'mxc://t2bot.io/3076746d3bda4a0bd581bf1bcd3f04de', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Money', description: 'Flicking through a wad of cash grinningly', imageMxc: 'mxc://t2bot.io/9c714fb6fa2c12ed9c00a71e442e50fb', thumbnailMxc: 'mxc://t2bot.io/9c714fb6fa2c12ed9c00a71e442e50fb', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Ponder', description: 'Thinking very hard', imageMxc: 'mxc://t2bot.io/921a1ea7ef2c33c730e1b02c06a0818f', thumbnailMxc: 'mxc://t2bot.io/921a1ea7ef2c33c730e1b02c06a0818f', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Popcorn', description: 'Frantically eats popcorn', imageMxc: 'mxc://t2bot.io/5a0d63ca0546135cc361f9a48300a198', thumbnailMxc: 'mxc://t2bot.io/5a0d63ca0546135cc361f9a48300a198', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Relief', description: 'Clutching phone with nervous exhale of relief', imageMxc: 'mxc://t2bot.io/3880de09c46442e090cf1918abc2e511', thumbnailMxc: 'mxc://t2bot.io/3880de09c46442e090cf1918abc2e511', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Say Wha', description: 'Looks to the camera with a \'are you serious?\' look', imageMxc: 'mxc://t2bot.io/55d8e2b01d19f0405a59397caca3a648', thumbnailMxc: 'mxc://t2bot.io/55d8e2b01d19f0405a59397caca3a648', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Scared Cam', description: 'Clutching video camera with a terrified look', imageMxc: 'mxc://t2bot.io/f3b6726c375ca1888a05c47f33a43d18', thumbnailMxc: 'mxc://t2bot.io/f3b6726c375ca1888a05c47f33a43d18', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Scream', description: 'Head back screaming with arms in the air', imageMxc: 'mxc://t2bot.io/d0d65ccff76788eff4450f590917267e', thumbnailMxc: 'mxc://t2bot.io/d0d65ccff76788eff4450f590917267e', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Sick', description: 'Clutching bucket about to vomit', imageMxc: 'mxc://t2bot.io/6bcb81672ed49eaab462001cece30a3f', thumbnailMxc: 'mxc://t2bot.io/6bcb81672ed49eaab462001cece30a3f', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Smug', description: 'Grinning to the side with sparkles', imageMxc: 'mxc://t2bot.io/0313a9f2d54f0db0a282f05a2a1a2e69', thumbnailMxc: 'mxc://t2bot.io/0313a9f2d54f0db0a282f05a2a1a2e69', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Spider', description: 'Smiling spider with a hat', imageMxc: 'mxc://t2bot.io/0404bf77e66df2b5664d024f3c50a269', thumbnailMxc: 'mxc://t2bot.io/0404bf77e66df2b5664d024f3c50a269', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Spy', description: 'Looking from behind a wall suspiciously', imageMxc: 'mxc://t2bot.io/d6f4b554e2c8265416c1a877b2aba6ea', thumbnailMxc: 'mxc://t2bot.io/d6f4b554e2c8265416c1a877b2aba6ea', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Srs', description: 'Fed up and hunched over with a blank stare', imageMxc: 'mxc://t2bot.io/a55955aadcda3a7910976903e20dc76e', thumbnailMxc: 'mxc://t2bot.io/a55955aadcda3a7910976903e20dc76e', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Strong', description: 'Tough guy with shades and stubble', imageMxc: 'mxc://t2bot.io/efdd23e0d2822fd98946e0d764230d8b', thumbnailMxc: 'mxc://t2bot.io/efdd23e0d2822fd98946e0d764230d8b', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Sweat', description: 'A nervous panicky sweaty forced smile', imageMxc: 'mxc://t2bot.io/63387d6c81ed752632781bbdb52faef2', thumbnailMxc: 'mxc://t2bot.io/63387d6c81ed752632781bbdb52faef2', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'VR', description: 'Playing with virtual reality', imageMxc: 'mxc://t2bot.io/d076a025e0d67087f8be75f9c1fdde95', thumbnailMxc: 'mxc://t2bot.io/d076a025e0d67087f8be75f9c1fdde95', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Wait', description: 'Holding phone waiting for a response with a frown', imageMxc: 'mxc://t2bot.io/fcd6dbb649010faef17414f7c6c5611e', thumbnailMxc: 'mxc://t2bot.io/fcd6dbb649010faef17414f7c6c5611e', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'WTF', description: 'Screaming with disgust', imageMxc: 'mxc://t2bot.io/c860ad07370d5986f25ab5427e2d3146', thumbnailMxc: 'mxc://t2bot.io/c860ad07370d5986f25ab5427e2d3146', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Yeah', description: 'Pointing toward with enthusiasm', imageMxc: 'mxc://t2bot.io/4676643f47a448654e7ba55d0c61a9fd', thumbnailMxc: 'mxc://t2bot.io/4676643f47a448654e7ba55d0c61a9fd', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{ packId: packId, name: 'Yell', description: 'Angrily yelling', imageMxc: 'mxc://t2bot.io/75b451684e4ae0d53427b5c5db2f2953', thumbnailMxc: 'mxc://t2bot.io/75b451684e4ae0d53427b5c5db2f2953', mimetype: 'image/png', thumbnailWidth: 512, thumbnailHeight: 512 },
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Argue',
|
||||
description: 'Two people arguing in a heated debate',
|
||||
imageMxc: 'mxc://t2bot.io/cfe97ad50ee1f35de322306f58d9d4a1',
|
||||
thumbnailMxc: 'mxc://t2bot.io/cfe97ad50ee1f35de322306f58d9d4a1',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Awake',
|
||||
description: 'Lying awake at night',
|
||||
imageMxc: 'mxc://t2bot.io/0046a75ce93d1322cf9577ea11a49f04',
|
||||
thumbnailMxc: 'mxc://t2bot.io/0046a75ce93d1322cf9577ea11a49f04',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Bed',
|
||||
description: 'Crying yourself to sleep',
|
||||
imageMxc: 'mxc://t2bot.io/fadda48f250674af3f7e3bee55c91b80',
|
||||
thumbnailMxc: 'mxc://t2bot.io/fadda48f250674af3f7e3bee55c91b80',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Bird',
|
||||
description: 'Angry bird',
|
||||
imageMxc: 'mxc://t2bot.io/8a8603d8d6c375e0e92171fc24e37e49',
|
||||
thumbnailMxc: 'mxc://t2bot.io/35eb7a9e416bc5634fac5c9f0f4b0ced',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Blush',
|
||||
description: 'Blushing and looking away',
|
||||
imageMxc: 'mxc://t2bot.io/535d46b7834885bf13bd9ff833561ce5',
|
||||
thumbnailMxc: 'mxc://t2bot.io/535d46b7834885bf13bd9ff833561ce5',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Celebrate',
|
||||
description: 'Opening up a bottle of champagne',
|
||||
imageMxc: 'mxc://t2bot.io/1377f37c0b6a5c401483c4be99e48938',
|
||||
thumbnailMxc: 'mxc://t2bot.io/1377f37c0b6a5c401483c4be99e48938',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Cold',
|
||||
description: 'Shivering in the cold',
|
||||
imageMxc: 'mxc://t2bot.io/3ba28460aae70d33dd061b3a000f5ee2',
|
||||
thumbnailMxc: 'mxc://t2bot.io/3ba28460aae70d33dd061b3a000f5ee2',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Dead Inside',
|
||||
description: 'I feel dead inside',
|
||||
imageMxc: 'mxc://t2bot.io/15196e0235422de488f2d3909054ab82',
|
||||
thumbnailMxc: 'mxc://t2bot.io/15196e0235422de488f2d3909054ab82',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Detective',
|
||||
description: 'A pondering detective',
|
||||
imageMxc: 'mxc://t2bot.io/5042e46fa9d514694c6cbd0a8a1b23b5',
|
||||
thumbnailMxc: 'mxc://t2bot.io/5042e46fa9d514694c6cbd0a8a1b23b5',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Distraught',
|
||||
description: 'Screaming on my knees',
|
||||
imageMxc: 'mxc://t2bot.io/a4173bc5a9ea16528c8442e800513597',
|
||||
thumbnailMxc: 'mxc://t2bot.io/a4173bc5a9ea16528c8442e800513597',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Donut',
|
||||
description: 'Eating a donut',
|
||||
imageMxc: 'mxc://t2bot.io/e3ca7aaa7318c141fd8d2f0a7cd2c52e',
|
||||
thumbnailMxc: 'mxc://t2bot.io/e3ca7aaa7318c141fd8d2f0a7cd2c52e',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Drawing',
|
||||
description: 'Drawing on a sketchbook',
|
||||
imageMxc: 'mxc://t2bot.io/a6327bc46b23684398ccbfcbedd78c66',
|
||||
thumbnailMxc: 'mxc://t2bot.io/a6327bc46b23684398ccbfcbedd78c66',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Driving',
|
||||
description: 'Angrily driving',
|
||||
imageMxc: 'mxc://t2bot.io/3275e023a6683075a58eea3d991e4673',
|
||||
thumbnailMxc: 'mxc://t2bot.io/3275e023a6683075a58eea3d991e4673',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Excellent',
|
||||
description: 'Pleased with the results',
|
||||
imageMxc: 'mxc://t2bot.io/311ae79d33cdde34ca4241d40a0e6aaf',
|
||||
thumbnailMxc: 'mxc://t2bot.io/311ae79d33cdde34ca4241d40a0e6aaf',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Flower',
|
||||
description: 'Smells like bees',
|
||||
imageMxc: 'mxc://t2bot.io/b6ac9eb8e5377b54f61cd8de9b36edc7',
|
||||
thumbnailMxc: 'mxc://t2bot.io/b6ac9eb8e5377b54f61cd8de9b36edc7',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Gaming',
|
||||
description: 'Playing a game',
|
||||
imageMxc: 'mxc://t2bot.io/bfd7e0fe2ef7af961cb93ae28c0fda69',
|
||||
thumbnailMxc: 'mxc://t2bot.io/bfd7e0fe2ef7af961cb93ae28c0fda69',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Get Outta Here',
|
||||
description: 'Waving it off with a laugh',
|
||||
imageMxc: 'mxc://t2bot.io/a77772dde35fc7a67a782dfc0e8e9ed9',
|
||||
thumbnailMxc: 'mxc://t2bot.io/a77772dde35fc7a67a782dfc0e8e9ed9',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Heartbroken',
|
||||
description: 'My heart has broken',
|
||||
imageMxc: 'mxc://t2bot.io/d36a5f1255c5f34d5e2e5985d0c38a68',
|
||||
thumbnailMxc: 'mxc://t2bot.io/d36a5f1255c5f34d5e2e5985d0c38a68',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Hi',
|
||||
description: 'A friendly wave',
|
||||
imageMxc: 'mxc://t2bot.io/9b74aaaec56ebb8b4d483fc1840827be',
|
||||
thumbnailMxc: 'mxc://t2bot.io/9b74aaaec56ebb8b4d483fc1840827be',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Hide',
|
||||
description: 'Nervously tries to hide away and look discreet',
|
||||
imageMxc: 'mxc://t2bot.io/032d8648065f36a2155654b3d59ab8bf',
|
||||
thumbnailMxc: 'mxc://t2bot.io/032d8648065f36a2155654b3d59ab8bf',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'J',
|
||||
description: 'Mouth agape',
|
||||
imageMxc: 'mxc://t2bot.io/c27889f840a49eb42781ffa1fe692b0d',
|
||||
thumbnailMxc: 'mxc://t2bot.io/c27889f840a49eb42781ffa1fe692b0d',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Keyboard cat',
|
||||
description: 'A cat sits at a keybaord with a confused look',
|
||||
imageMxc: 'mxc://t2bot.io/7d79434b361aa00f9d7de99a4722cc9f',
|
||||
thumbnailMxc: 'mxc://t2bot.io/7d79434b361aa00f9d7de99a4722cc9f',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Lewd',
|
||||
description: 'Blushingly overwhelmed',
|
||||
imageMxc: 'mxc://t2bot.io/3076746d3bda4a0bd581bf1bcd3f04de',
|
||||
thumbnailMxc: 'mxc://t2bot.io/3076746d3bda4a0bd581bf1bcd3f04de',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Money',
|
||||
description: 'Flicking through a wad of cash grinningly',
|
||||
imageMxc: 'mxc://t2bot.io/9c714fb6fa2c12ed9c00a71e442e50fb',
|
||||
thumbnailMxc: 'mxc://t2bot.io/9c714fb6fa2c12ed9c00a71e442e50fb',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Ponder',
|
||||
description: 'Thinking very hard',
|
||||
imageMxc: 'mxc://t2bot.io/921a1ea7ef2c33c730e1b02c06a0818f',
|
||||
thumbnailMxc: 'mxc://t2bot.io/921a1ea7ef2c33c730e1b02c06a0818f',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Popcorn',
|
||||
description: 'Frantically eats popcorn',
|
||||
imageMxc: 'mxc://t2bot.io/5a0d63ca0546135cc361f9a48300a198',
|
||||
thumbnailMxc: 'mxc://t2bot.io/5a0d63ca0546135cc361f9a48300a198',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Relief',
|
||||
description: 'Clutching phone with nervous exhale of relief',
|
||||
imageMxc: 'mxc://t2bot.io/3880de09c46442e090cf1918abc2e511',
|
||||
thumbnailMxc: 'mxc://t2bot.io/3880de09c46442e090cf1918abc2e511',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Say Wha',
|
||||
description: 'Looks to the camera with a \'are you serious?\' look',
|
||||
imageMxc: 'mxc://t2bot.io/55d8e2b01d19f0405a59397caca3a648',
|
||||
thumbnailMxc: 'mxc://t2bot.io/55d8e2b01d19f0405a59397caca3a648',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Scared Cam',
|
||||
description: 'Clutching video camera with a terrified look',
|
||||
imageMxc: 'mxc://t2bot.io/f3b6726c375ca1888a05c47f33a43d18',
|
||||
thumbnailMxc: 'mxc://t2bot.io/f3b6726c375ca1888a05c47f33a43d18',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Scream',
|
||||
description: 'Head back screaming with arms in the air',
|
||||
imageMxc: 'mxc://t2bot.io/d0d65ccff76788eff4450f590917267e',
|
||||
thumbnailMxc: 'mxc://t2bot.io/d0d65ccff76788eff4450f590917267e',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Sick',
|
||||
description: 'Clutching bucket about to vomit',
|
||||
imageMxc: 'mxc://t2bot.io/6bcb81672ed49eaab462001cece30a3f',
|
||||
thumbnailMxc: 'mxc://t2bot.io/6bcb81672ed49eaab462001cece30a3f',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Smug',
|
||||
description: 'Grinning to the side with sparkles',
|
||||
imageMxc: 'mxc://t2bot.io/0313a9f2d54f0db0a282f05a2a1a2e69',
|
||||
thumbnailMxc: 'mxc://t2bot.io/0313a9f2d54f0db0a282f05a2a1a2e69',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Spider',
|
||||
description: 'Smiling spider with a hat',
|
||||
imageMxc: 'mxc://t2bot.io/0404bf77e66df2b5664d024f3c50a269',
|
||||
thumbnailMxc: 'mxc://t2bot.io/0404bf77e66df2b5664d024f3c50a269',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Spy',
|
||||
description: 'Looking from behind a wall suspiciously',
|
||||
imageMxc: 'mxc://t2bot.io/d6f4b554e2c8265416c1a877b2aba6ea',
|
||||
thumbnailMxc: 'mxc://t2bot.io/d6f4b554e2c8265416c1a877b2aba6ea',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Srs',
|
||||
description: 'Fed up and hunched over with a blank stare',
|
||||
imageMxc: 'mxc://t2bot.io/a55955aadcda3a7910976903e20dc76e',
|
||||
thumbnailMxc: 'mxc://t2bot.io/a55955aadcda3a7910976903e20dc76e',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Strong',
|
||||
description: 'Tough guy with shades and stubble',
|
||||
imageMxc: 'mxc://t2bot.io/efdd23e0d2822fd98946e0d764230d8b',
|
||||
thumbnailMxc: 'mxc://t2bot.io/efdd23e0d2822fd98946e0d764230d8b',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Sweat',
|
||||
description: 'A nervous panicky sweaty forced smile',
|
||||
imageMxc: 'mxc://t2bot.io/63387d6c81ed752632781bbdb52faef2',
|
||||
thumbnailMxc: 'mxc://t2bot.io/63387d6c81ed752632781bbdb52faef2',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'VR',
|
||||
description: 'Playing with virtual reality',
|
||||
imageMxc: 'mxc://t2bot.io/d076a025e0d67087f8be75f9c1fdde95',
|
||||
thumbnailMxc: 'mxc://t2bot.io/d076a025e0d67087f8be75f9c1fdde95',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Wait',
|
||||
description: 'Holding phone waiting for a response with a frown',
|
||||
imageMxc: 'mxc://t2bot.io/fcd6dbb649010faef17414f7c6c5611e',
|
||||
thumbnailMxc: 'mxc://t2bot.io/fcd6dbb649010faef17414f7c6c5611e',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'WTF',
|
||||
description: 'Screaming with disgust',
|
||||
imageMxc: 'mxc://t2bot.io/c860ad07370d5986f25ab5427e2d3146',
|
||||
thumbnailMxc: 'mxc://t2bot.io/c860ad07370d5986f25ab5427e2d3146',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Yeah',
|
||||
description: 'Pointing toward with enthusiasm',
|
||||
imageMxc: 'mxc://t2bot.io/4676643f47a448654e7ba55d0c61a9fd',
|
||||
thumbnailMxc: 'mxc://t2bot.io/4676643f47a448654e7ba55d0c61a9fd',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
{
|
||||
packId: packId,
|
||||
name: 'Yell',
|
||||
description: 'Angrily yelling',
|
||||
imageMxc: 'mxc://t2bot.io/75b451684e4ae0d53427b5c5db2f2953',
|
||||
thumbnailMxc: 'mxc://t2bot.io/75b451684e4ae0d53427b5c5db2f2953',
|
||||
mimetype: 'image/png',
|
||||
thumbnailWidth: 512,
|
||||
thumbnailHeight: 512
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
31
src/db/migrations/20190630194345-AddTermsOfService.ts
Normal file
31
src/db/migrations/20190630194345-AddTermsOfService.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_terms", {
|
||||
// Ideally we'd use a composite primary key here, but that's not really possible with our libraries.
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"shortcode": {type: DataType.STRING, allowNull: false},
|
||||
"version": {type: DataType.STRING, allowNull: false},
|
||||
}))
|
||||
.then(() => queryInterface.createTable("dimension_terms_text", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"termsId": {
|
||||
type: DataType.INTEGER, allowNull: false,
|
||||
references: {model: "dimension_terms", key: "id"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
"language": {type: DataType.STRING, allowNull: false},
|
||||
"name": {type: DataType.STRING, allowNull: false},
|
||||
"text": {type: DataType.STRING, allowNull: true},
|
||||
"url": {type: DataType.STRING, allowNull: false},
|
||||
}));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.dropTable("dimension_terms"))
|
||||
.then(() => queryInterface.dropTable("dimension_terms_text"));
|
||||
}
|
||||
}
|
25
src/db/migrations/20190706154345-AddUserSignedTerms.ts
Normal file
25
src/db/migrations/20190706154345-AddUserSignedTerms.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_terms_signed", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"termsId": {
|
||||
type: DataType.INTEGER, allowNull: false,
|
||||
references: {model: "dimension_terms", key: "id"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
"userId": {
|
||||
type: DataType.STRING, allowNull: false,
|
||||
references: {model: "dimension_users", key: "userId"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
}));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.dropTable("dimension_terms_signed"));
|
||||
}
|
||||
}
|
21
src/db/migrations/20190710213945-AddUpstreamTermsCache.ts
Normal file
21
src/db/migrations/20190710213945-AddUpstreamTermsCache.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { QueryInterface } from "sequelize";
|
||||
import { DataType } from "sequelize-typescript";
|
||||
|
||||
export default {
|
||||
up: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.createTable("dimension_terms_upstream", {
|
||||
"id": {type: DataType.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
|
||||
"upstreamId": {
|
||||
type: DataType.INTEGER, allowNull: false,
|
||||
references: {model: "dimension_upstreams", key: "id"},
|
||||
onUpdate: "cascade", onDelete: "cascade",
|
||||
},
|
||||
"url": {type: DataType.STRING, allowNull: false},
|
||||
}));
|
||||
},
|
||||
down: (queryInterface: QueryInterface) => {
|
||||
return Promise.resolve()
|
||||
.then(() => queryInterface.dropTable("dimension_terms_upstream"));
|
||||
}
|
||||
}
|
23
src/db/models/TermsRecord.ts
Normal file
23
src/db/models/TermsRecord.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { AutoIncrement, Column, HasMany, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||
import TermsTextRecord from "./TermsTextRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_terms",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class TermsRecord extends Model<TermsRecord> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@Column
|
||||
shortcode: string;
|
||||
|
||||
@Column
|
||||
version: string;
|
||||
|
||||
@HasMany(() => TermsTextRecord)
|
||||
texts: TermsTextRecord[];
|
||||
}
|
25
src/db/models/TermsSignedRecord.ts
Normal file
25
src/db/models/TermsSignedRecord.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { AllowNull, AutoIncrement, Column, ForeignKey, Model, PrimaryKey, Table } from "sequelize-typescript";
|
||||
import User from "./User";
|
||||
import TermsRecord from "./TermsRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_terms_signed",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class TermsSignedRecord extends Model<TermsSignedRecord> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
@ForeignKey(() => TermsRecord)
|
||||
termsId?: number;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
@ForeignKey(() => User)
|
||||
userId?: string;
|
||||
}
|
44
src/db/models/TermsTextRecord.ts
Normal file
44
src/db/models/TermsTextRecord.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
AllowNull,
|
||||
AutoIncrement,
|
||||
BelongsTo,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table
|
||||
} from "sequelize-typescript";
|
||||
import TermsRecord from "./TermsRecord";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_terms_text",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class TermsTextRecord extends Model<TermsTextRecord> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
@ForeignKey(() => TermsRecord)
|
||||
termsId?: number;
|
||||
|
||||
@BelongsTo(() => TermsRecord)
|
||||
terms: TermsRecord;
|
||||
|
||||
@Column
|
||||
language: string;
|
||||
|
||||
@Column
|
||||
url: string;
|
||||
|
||||
@Column
|
||||
name: string;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
text?: string;
|
||||
}
|
34
src/db/models/TermsUpstreamRecord.ts
Normal file
34
src/db/models/TermsUpstreamRecord.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
AllowNull,
|
||||
AutoIncrement,
|
||||
BelongsTo,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table
|
||||
} from "sequelize-typescript";
|
||||
import Upstream from "./Upstream";
|
||||
|
||||
@Table({
|
||||
tableName: "dimension_terms_upstream",
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
})
|
||||
export default class TermsUpstreamRecord extends Model<TermsUpstreamRecord> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column
|
||||
id: number;
|
||||
|
||||
@AllowNull
|
||||
@Column
|
||||
@ForeignKey(() => Upstream)
|
||||
upstreamId?: number;
|
||||
|
||||
@BelongsTo(() => Upstream)
|
||||
upstream: Upstream;
|
||||
|
||||
@Column
|
||||
url: string;
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
import {
|
||||
AllowNull, AutoIncrement, BelongsTo, Column, ForeignKey, Model, PrimaryKey,
|
||||
AllowNull,
|
||||
AutoIncrement,
|
||||
BelongsTo,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table
|
||||
} from "sequelize-typescript";
|
||||
import User from "./User";
|
||||
@ -33,4 +39,7 @@ export default class UserScalarToken extends Model<UserScalarToken> {
|
||||
@Column
|
||||
@ForeignKey(() => Upstream)
|
||||
upstreamId?: number;
|
||||
|
||||
@BelongsTo(() => Upstream)
|
||||
upstream: Upstream;
|
||||
}
|
@ -5,4 +5,8 @@ export interface ScalarRegisterResponse {
|
||||
export interface ScalarAccountResponse {
|
||||
user_id: string;
|
||||
// credit: number; // present on scalar-web
|
||||
}
|
||||
|
||||
export interface ScalarLogoutResponse {
|
||||
// Nothing of interest
|
||||
}
|
@ -349,7 +349,7 @@ export class NebProxy {
|
||||
LogService.error("NebProxy", res.body);
|
||||
reject(new Error("Request failed"));
|
||||
} else {
|
||||
if (typeof(res.body) === "string") res.body = JSON.parse(res.body);
|
||||
if (typeof (res.body) === "string") res.body = JSON.parse(res.body);
|
||||
resolve(res.body);
|
||||
}
|
||||
});
|
||||
|
@ -1,21 +1,64 @@
|
||||
import { OpenId } from "../models/OpenId";
|
||||
import { ScalarAccountResponse, ScalarRegisterResponse } from "../models/ScalarResponses";
|
||||
import { ScalarAccountResponse, ScalarLogoutResponse, ScalarRegisterResponse } from "../models/ScalarResponses";
|
||||
import * as request from "request";
|
||||
import { LogService } from "matrix-js-snippets";
|
||||
import Upstream from "../db/models/Upstream";
|
||||
import { SCALAR_API_VERSION } from "../utils/common-constants";
|
||||
import { ITermsResponse } from "../api/controllers/TermsController";
|
||||
import { subscriptionLogsToBeFn } from "rxjs/internal/testing/TestScheduler";
|
||||
|
||||
const REGISTER_ROUTE = "/register";
|
||||
const ACCOUNT_INFO_ROUTE = "/account";
|
||||
const LOGOUT_ROUTE = "/logout";
|
||||
const TERMS_ROUTE = "/terms";
|
||||
|
||||
export class ScalarClient {
|
||||
constructor(private upstream: Upstream) {
|
||||
public static readonly KIND_LEGACY = "legacy";
|
||||
public static readonly KIND_MATRIX_V1 = "matrix_v1";
|
||||
|
||||
constructor(private upstream: Upstream, private kind = ScalarClient.KIND_LEGACY) {
|
||||
}
|
||||
|
||||
private makeRequestArguments(path: string, token: string): { scalarUrl: string, headers: any, queryString: any } {
|
||||
if (this.kind === ScalarClient.KIND_LEGACY) {
|
||||
const addlQuery = {};
|
||||
if (token) addlQuery['scalar_token'] = token;
|
||||
return {
|
||||
scalarUrl: this.upstream.scalarUrl + path,
|
||||
headers: {},
|
||||
queryString: {
|
||||
v: SCALAR_API_VERSION,
|
||||
...addlQuery,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const parsed = new URL(this.upstream.scalarUrl);
|
||||
if (path === ACCOUNT_INFO_ROUTE || path === TERMS_ROUTE) {
|
||||
parsed.pathname = `/_matrix/integrations/v1${path}`;
|
||||
} else {
|
||||
parsed.pathname = `/_matrix/integrations/v1${ACCOUNT_INFO_ROUTE}${path}`;
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
return {
|
||||
scalarUrl: parsed.toString(),
|
||||
headers: headers,
|
||||
queryString: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public register(openId: OpenId): Promise<ScalarRegisterResponse> {
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: " + this.upstream.scalarUrl + "/register");
|
||||
const {scalarUrl, headers, queryString} = this.makeRequestArguments(REGISTER_ROUTE, null);
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: " + scalarUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "POST",
|
||||
url: this.upstream.scalarUrl + "/register",
|
||||
qs: {v: SCALAR_API_VERSION},
|
||||
url: scalarUrl,
|
||||
qs: queryString,
|
||||
headers: headers,
|
||||
json: openId,
|
||||
}, (err, res, _body) => {
|
||||
if (err) {
|
||||
@ -33,12 +76,14 @@ export class ScalarClient {
|
||||
}
|
||||
|
||||
public getAccount(token: string): Promise<ScalarAccountResponse> {
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: " + this.upstream.scalarUrl + "/account");
|
||||
const {scalarUrl, headers, queryString} = this.makeRequestArguments(ACCOUNT_INFO_ROUTE, token);
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: " + scalarUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "GET",
|
||||
url: this.upstream.scalarUrl + "/account",
|
||||
qs: {v: SCALAR_API_VERSION, scalar_token: token},
|
||||
url: scalarUrl,
|
||||
qs: queryString,
|
||||
headers: headers,
|
||||
json: true,
|
||||
}, (err, res, _body) => {
|
||||
if (err) {
|
||||
@ -46,7 +91,90 @@ export class ScalarClient {
|
||||
LogService.error("ScalarClient", err);
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
if (typeof(res.body) === 'string') {
|
||||
try {
|
||||
res.body = JSON.parse(res.body);
|
||||
} catch (e) {
|
||||
LogService.error("ScalarClient", "Got error parsing error response:");
|
||||
LogService.error("ScalarClient", e);
|
||||
}
|
||||
}
|
||||
LogService.error("ScalarClient", "Got status code " + res.statusCode + " while getting information for token");
|
||||
reject(res);
|
||||
} else {
|
||||
resolve(res.body);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public logout(token: string): Promise<ScalarLogoutResponse> {
|
||||
const {scalarUrl, headers, queryString} = this.makeRequestArguments(LOGOUT_ROUTE, token);
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: " + scalarUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "POST",
|
||||
url: scalarUrl,
|
||||
qs: queryString,
|
||||
headers: headers,
|
||||
json: true,
|
||||
}, (err, res, _body) => {
|
||||
if (err) {
|
||||
LogService.error("ScalarClient", "Error logging out token");
|
||||
LogService.error("ScalarClient", err);
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
LogService.error("ScalarClient", "Got status code " + res.statusCode + " while logging out token");
|
||||
reject(res.statusCode);
|
||||
} else {
|
||||
resolve(res.body);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getAvailableTerms(): Promise<ITermsResponse> {
|
||||
const {scalarUrl, headers, queryString} = this.makeRequestArguments(TERMS_ROUTE, null);
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: GET " + scalarUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "GET",
|
||||
url: scalarUrl,
|
||||
qs: queryString,
|
||||
headers: headers,
|
||||
json: true,
|
||||
}, (err, res, _body) => {
|
||||
if (err) {
|
||||
LogService.error("ScalarClient", "Error getting terms for token");
|
||||
LogService.error("ScalarClient", err);
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
LogService.error("ScalarClient", "Got status code " + res.statusCode + " while getting terms for token");
|
||||
reject(res.statusCode);
|
||||
} else {
|
||||
resolve(res.body);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public signTermsUrls(token: string, urls: string[]): Promise<any> {
|
||||
const {scalarUrl, headers, queryString} = this.makeRequestArguments(TERMS_ROUTE, token);
|
||||
LogService.info("ScalarClient", "Doing upstream scalar request: POST " + scalarUrl);
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
method: "POST",
|
||||
url: scalarUrl,
|
||||
qs: queryString,
|
||||
headers: headers,
|
||||
json: {user_accepts: urls},
|
||||
}, (err, res, _body) => {
|
||||
if (err) {
|
||||
LogService.error("ScalarClient", "Error updating terms for token");
|
||||
LogService.error("ScalarClient", err);
|
||||
reject(err);
|
||||
} else if (res.statusCode !== 200) {
|
||||
LogService.error("ScalarClient", "Got status code " + res.statusCode + " while updating terms for token");
|
||||
reject(res.statusCode);
|
||||
} else {
|
||||
resolve(res.body);
|
||||
|
5
src/utils/hashing.ts
Normal file
5
src/utils/hashing.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as crypto from "crypto";
|
||||
|
||||
export function md5(text: string): string {
|
||||
return crypto.createHash("md5").update(text).digest('hex').toLowerCase();
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
<li (click)="goto('custom-bots')" [ngClass]="[isActive('custom-bots') ? 'active' : '']">Custom Bots</li>
|
||||
<li (click)="goto('bridges')" [ngClass]="[isActive('bridges') ? 'active' : '']">Bridges</li>
|
||||
<li (click)="goto('stickerpacks')" [ngClass]="[isActive('stickerpacks') ? 'active' : '']">Sticker Packs</li>
|
||||
<li (click)="goto('terms')" [ngClass]="[isActive('terms') ? 'active' : '']">Terms of Service</li>
|
||||
</ul>
|
||||
<span class="version">{{ version }}</span>
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
<div class="my-ibox-content">
|
||||
<p>
|
||||
<a href="https://github.com/matrix-org/matrix-appservice-gitter" target="_blank">matrix-appservice-gitter</a>
|
||||
is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a single
|
||||
bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.
|
||||
is a Gitter bridge that supports bridging Gitter rooms to Matrix. Users on Matrix are represented as a
|
||||
single bot user in Gitter, however Gitter users are represented as real-looking Matrix users in the room.
|
||||
</p>
|
||||
|
||||
<table class="table table-striped table-condensed table-bordered">
|
||||
@ -24,7 +24,8 @@
|
||||
<tr *ngFor="let bridge of configurations trackById">
|
||||
<td>
|
||||
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||
<span class="text-muted" style="display: inline-block;" *ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
<span class="text-muted" style="display: inline-block;"
|
||||
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="editButton" (click)="editBridge(bridge)" *ngIf="!bridge.upstreamId">
|
||||
@ -34,10 +35,12 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()" [disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()"
|
||||
[disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<i class="fa fa-plus"></i> Add matrix.org's bridge
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()" [disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
|
||||
[disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<i class="fa fa-plus"></i> Add self-hosted bridge
|
||||
</button>
|
||||
</div>
|
||||
|
@ -3,7 +3,10 @@
|
||||
<h4>{{ isAdding ? "Add a new" : "Edit" }} self-hosted Gitter bridge</h4>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p>Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public internet.</p>
|
||||
<p>
|
||||
Self-hosted Gitter bridges already have provisioning enabled. Be careful not to expose the API to the public
|
||||
internet.
|
||||
</p>
|
||||
|
||||
<label class="label-block">
|
||||
Provisioning URL
|
||||
|
@ -25,7 +25,8 @@
|
||||
<tr *ngFor="let bridge of configurations trackById">
|
||||
<td>
|
||||
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||
<span class="text-muted" style="display: inline-block;" *ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
<span class="text-muted" style="display: inline-block;"
|
||||
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
</td>
|
||||
<td *ngIf="bridge.isOnline">
|
||||
{{ getEnabledNetworksString(bridge) }}
|
||||
@ -42,7 +43,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()" *ngIf="!hasModularBridge">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()"
|
||||
*ngIf="!hasModularBridge">
|
||||
<i class="fa fa-plus"></i> Add matrix.org's bridge
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()">
|
||||
|
@ -3,7 +3,10 @@
|
||||
<h4>{{ isAdding ? "Add a new" : "Edit" }} self-hosted Slack bridge</h4>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p>Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.</p>
|
||||
<p>
|
||||
Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public
|
||||
internet.
|
||||
</p>
|
||||
|
||||
<label class="label-block">
|
||||
Provisioning URL
|
||||
|
@ -5,9 +5,10 @@
|
||||
<my-ibox boxTitle="Slack Bridge Configurations">
|
||||
<div class="my-ibox-content">
|
||||
<p>
|
||||
<a href="https://github.com/matrix-org/matrix-appservice-slack" target="_blank">matrix-appservice-slack</a>
|
||||
is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access their
|
||||
Slack workspaces and from there they can pick the channels they'd like to bridge.
|
||||
<a href="https://github.com/matrix-org/matrix-appservice-slack"
|
||||
target="_blank">matrix-appservice-slack</a>
|
||||
is a Slack bridge that supports bridging Slack channels to Matrix. Users authorize the bridge to access
|
||||
their Slack workspaces and from there they can pick the channels they'd like to bridge.
|
||||
</p>
|
||||
|
||||
<table class="table table-striped table-condensed table-bordered">
|
||||
@ -24,7 +25,8 @@
|
||||
<tr *ngFor="let bridge of configurations trackById">
|
||||
<td>
|
||||
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||
<span class="text-muted" style="display: inline-block;" *ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
<span class="text-muted" style="display: inline-block;"
|
||||
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="editButton" (click)="editBridge(bridge)" *ngIf="!bridge.upstreamId">
|
||||
@ -34,10 +36,12 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()" [disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addModularHostedBridge()"
|
||||
[disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<i class="fa fa-plus"></i> Add matrix.org's bridge
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()" [disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
|
||||
[disabled]="(configurations && configurations.length > 0) || isUpdating">
|
||||
<i class="fa fa-plus"></i> Add self-hosted bridge
|
||||
</button>
|
||||
</div>
|
||||
|
@ -27,7 +27,8 @@
|
||||
<tr *ngFor="let bridge of configurations trackById">
|
||||
<td>
|
||||
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||
<span class="text-muted" style="display: inline-block;" *ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
<span class="text-muted" style="display: inline-block;"
|
||||
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ getEnabledFeaturesString(bridge) }}
|
||||
@ -40,7 +41,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()" [disabled]="configurations && configurations.length > 0">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
|
||||
[disabled]="configurations && configurations.length > 0">
|
||||
<i class="fa fa-plus"></i> Add self-hosted bridge
|
||||
</button>
|
||||
</div>
|
||||
|
@ -23,7 +23,8 @@
|
||||
<tr *ngFor="let bridge of configurations trackById">
|
||||
<td>
|
||||
{{ bridge.upstreamId ? "matrix.org's bridge" : "Self-hosted bridge" }}
|
||||
<span class="text-muted" style="display: inline-block;" *ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
<span class="text-muted" style="display: inline-block;"
|
||||
*ngIf="!bridge.upstreamId">({{ bridge.provisionUrl }})</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="editButton" (click)="editBridge(bridge)">
|
||||
@ -33,7 +34,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()" [disabled]="configurations && configurations.length > 0">
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="addSelfHostedBridge()"
|
||||
[disabled]="configurations && configurations.length > 0">
|
||||
<i class="fa fa-plus"></i> Add self-hosted bridge
|
||||
</button>
|
||||
</div>
|
||||
|
@ -4,7 +4,10 @@
|
||||
<div *ngIf="!isLoading">
|
||||
<my-ibox boxTitle="Configuration">
|
||||
<div class="my-ibox-content">
|
||||
<p>Parts of your configuration are displayed below. To change these values, edit your configuration and restart Dimension.</p>
|
||||
<p>
|
||||
Parts of your configuration are displayed below. To change these values, edit your configuration and
|
||||
restart Dimension.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@ -20,19 +23,19 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>Homeserver</strong><br />
|
||||
Name: {{ config.homeserver.name }}<br />
|
||||
Federation URL: {{ config.homeserver.federationUrl }}<br />
|
||||
Federation Hostname: {{ config.homeserver.federationHostname }}<br />
|
||||
Client/Server URL: {{ config.homeserver.clientServerUrl }}<br />
|
||||
<strong>Homeserver</strong><br/>
|
||||
Name: {{ config.homeserver.name }}<br/>
|
||||
Federation URL: {{ config.homeserver.federationUrl }}<br/>
|
||||
Federation Hostname: {{ config.homeserver.federationHostname }}<br/>
|
||||
Client/Server URL: {{ config.homeserver.clientServerUrl }}<br/>
|
||||
Utility User ID: {{ config.homeserver.userId }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<strong>Sessions</strong><br />
|
||||
Tokens registered: {{ config.sessionInfo.numTokens }}<br />
|
||||
<strong>Sessions</strong><br/>
|
||||
Tokens registered: {{ config.sessionInfo.numTokens }}<br/>
|
||||
<button class="btn btn-danger btn-sm" type="button" (click)="logoutAll()">
|
||||
Logout Everyone
|
||||
</button>
|
||||
|
@ -5,8 +5,8 @@
|
||||
<div class="dialog-content">
|
||||
<p>
|
||||
Logging everyone out will disable all known login tokens for Dimension and upstream integration managers.
|
||||
Most clients will automatically re-register for a login token behind the scenes, similar to how a login token
|
||||
was first acquired.
|
||||
Most clients will automatically re-register for a login token behind the scenes, similar to how a login
|
||||
token was first acquired.
|
||||
</p>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
|
@ -11,5 +11,6 @@ export class LogoutConfirmationDialogContext extends BSModalContext {
|
||||
})
|
||||
export class AdminLogoutConfirmationDialogComponent implements ModalComponent<LogoutConfirmationDialogContext> {
|
||||
|
||||
constructor(public dialog: DialogRef<LogoutConfirmationDialogContext>) {}
|
||||
constructor(public dialog: DialogRef<LogoutConfirmationDialogContext>) {
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
<my-ibox boxTitle="New self-hosted go-neb">
|
||||
<div class="my-ibox-content">
|
||||
<p>Self-hosted go-neb instances are powered by application services installed on your homeserver. The application service is responsible for creating the bots dynamically.</p>
|
||||
<p>
|
||||
Self-hosted go-neb instances are powered by application services installed on your homeserver. The
|
||||
application service is responsible for creating the bots dynamically.
|
||||
</p>
|
||||
|
||||
<label class="label-block">
|
||||
User Prefix
|
||||
|
@ -17,7 +17,7 @@
|
||||
Image Size
|
||||
<span class="text-muted ">GIFs can be large, and sometimes it is more desirable to have them downsized.</span>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" [(ngModel)]="config.use_downsized" [disabled]="isUpdating" />
|
||||
<input type="checkbox" [(ngModel)]="config.use_downsized" [disabled]="isUpdating"/>
|
||||
Use downsized images
|
||||
</label>
|
||||
</label>
|
||||
|
@ -24,7 +24,8 @@
|
||||
<tr *ngFor="let neb of configurations trackById">
|
||||
<td>
|
||||
{{ neb.upstreamId ? "matrix.org's go-neb" : "Self-hosted go-neb" }}
|
||||
<span class="text-muted" style="display: inline-block;" *ngIf="!neb.upstreamId">({{ neb.adminUrl }})</span>
|
||||
<span class="text-muted" style="display: inline-block;"
|
||||
*ngIf="!neb.upstreamId">({{ neb.adminUrl }})</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ getEnabledBotsString(neb) }}
|
||||
|
@ -11,8 +11,9 @@
|
||||
</p>
|
||||
|
||||
<div class="input-group input-group-sm telegram-import">
|
||||
<input type="text" class="form-control" [(ngModel)]="tgUrl" placeholder="https://t.me/addstickers/YourPackID"
|
||||
[disabled]="isImporting" />
|
||||
<input type="text" class="form-control" [(ngModel)]="tgUrl"
|
||||
placeholder="https://t.me/addstickers/YourPackID"
|
||||
[disabled]="isImporting"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" (click)="startTelegramImport()" [disabled]="!tgUrl || isImporting">
|
||||
<i class="fa fa-download"></i>
|
||||
|
46
web/app/admin/terms/new-edit/new-edit.component.html
Normal file
46
web/app/admin/terms/new-edit/new-edit.component.html
Normal file
@ -0,0 +1,46 @@
|
||||
<div *ngIf="isLoading">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoading">
|
||||
<my-ibox *ngFor="let code of chosenLanguageCodes">
|
||||
<div class="my-ibox-title">
|
||||
<h5>{{languages[code].langName}} version</h5>
|
||||
</div>
|
||||
<div class="my-ibox-content">
|
||||
<label class="label-block">
|
||||
Name
|
||||
<span class="text-muted ">The translated name of your policy</span>
|
||||
<input type="text" class="form-control" placeholder="My Policy" [(ngModel)]="languages[code].name"
|
||||
[disabled]="isUpdating"/>
|
||||
</label>
|
||||
<label class="label-block">
|
||||
Policy text
|
||||
<span class="text-muted">This is where you put your policy's content.</span>
|
||||
</label>
|
||||
<ckeditor [editor]="Editor" [(ngModel)]="languages[code].text" [disabled]="isUpdating"></ckeditor>
|
||||
</div>
|
||||
</my-ibox>
|
||||
<!-- <my-ibox [hasTitle]="false">-->
|
||||
<!-- <div class="my-ibox-content buttons">-->
|
||||
<!-- <select [(ngModel)]="chosenLanguage" [disabled]="isUpdating" class="form-control form-control-sm">-->
|
||||
<!-- <option *ngFor="let lang of availableLanguages" [ngValue]="lang.code">{{lang.name}}</option>-->
|
||||
<!-- </select>-->
|
||||
<!-- <button type="button" (click)="addLanguage()" title="save" class="btn btn-outline-success btn-sm">-->
|
||||
<!-- <i class="fa fa-plus"></i> Add language-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </my-ibox>-->
|
||||
<my-ibox [hasTitle]="false">
|
||||
<div class="my-ibox-content buttons">
|
||||
<button type="button" (click)="create()" title="save" class="btn btn-primary btn-sm" *ngIf="!isEditing">
|
||||
<i class="far fa-save"></i> Create draft
|
||||
</button>
|
||||
<button type="button" (click)="create()" title="save" class="btn btn-primary btn-sm" *ngIf="isEditing">
|
||||
<i class="far fa-save"></i> Save draft
|
||||
</button>
|
||||
<button type="button" (click)="publish()" title="save" class="btn btn-primary btn-sm" *ngIf="isEditing">
|
||||
<i class="fas fa-upload"></i> Publish
|
||||
</button>
|
||||
</div>
|
||||
</my-ibox>
|
||||
</div>
|
8
web/app/admin/terms/new-edit/new-edit.component.scss
Normal file
8
web/app/admin/terms/new-edit/new-edit.component.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.buttons button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.buttons select {
|
||||
max-width: 200px;
|
||||
margin-bottom: 5px;
|
||||
}
|
213
web/app/admin/terms/new-edit/new-edit.component.ts
Normal file
213
web/app/admin/terms/new-edit/new-edit.component.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { AdminTermsApiService } from "../../../shared/services/admin/admin-terms-api.service";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import * as ClassicEditor from '@ckeditor/ckeditor5-build-classic';
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { Modal, overlayConfigFactory } from "ngx-modialog";
|
||||
import {
|
||||
AdminTermsNewEditPublishDialogComponent,
|
||||
AdminTermsNewEditPublishDialogContext
|
||||
} from "./publish/publish.component";
|
||||
|
||||
interface ILanguage {
|
||||
name: string,
|
||||
text: string,
|
||||
langName: string,
|
||||
url: string,
|
||||
isExternal: boolean,
|
||||
externalUrl: string,
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./new-edit.component.html",
|
||||
styleUrls: ["./new-edit.component.scss"],
|
||||
})
|
||||
export class AdminNewEditTermsComponent implements OnInit {
|
||||
|
||||
// TODO: Multiple language support
|
||||
// TODO: Support external URLs
|
||||
|
||||
private shortcode: string;
|
||||
|
||||
public Editor = ClassicEditor;
|
||||
|
||||
public isLoading = true;
|
||||
public isUpdating = false;
|
||||
public takenShortcodes: string[];
|
||||
public chosenLanguage: string = ISO6391.getAllCodes()[0];
|
||||
public languages: {
|
||||
[languageCode: string]: ILanguage;
|
||||
} = {
|
||||
"en": {
|
||||
name: "",
|
||||
text: "",
|
||||
langName: "English",
|
||||
url: "", // TODO: Calculate
|
||||
isExternal: false,
|
||||
externalUrl: "",
|
||||
},
|
||||
};
|
||||
public isEditing = false;
|
||||
|
||||
public get chosenLanguageCodes(): string[] {
|
||||
return Object.keys(this.languages);
|
||||
}
|
||||
|
||||
public get availableLanguages(): { name: string, code: string }[] {
|
||||
return ISO6391.getAllCodes()
|
||||
.filter(c => !this.chosenLanguageCodes.includes(c))
|
||||
.map(c => {
|
||||
return {code: c, name: ISO6391.getName(c)};
|
||||
});
|
||||
}
|
||||
|
||||
constructor(private adminTerms: AdminTermsApiService,
|
||||
private toaster: ToasterService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private modal: Modal) {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
let params = this.activatedRoute.snapshot.params;
|
||||
this.shortcode = params.shortcode;
|
||||
this.isEditing = !!this.shortcode;
|
||||
|
||||
if (this.isEditing) {
|
||||
this.adminTerms.getDraft(this.shortcode).then(policy => {
|
||||
this.shortcode = policy.shortcode;
|
||||
this.languages = {};
|
||||
|
||||
for (const code in policy.languages) {
|
||||
const i18nPolicy = policy.languages[code];
|
||||
this.languages[code] = {
|
||||
text: i18nPolicy.text,
|
||||
url: i18nPolicy.url,
|
||||
name: i18nPolicy.name,
|
||||
isExternal: false,
|
||||
langName: ISO6391.getName(code),
|
||||
externalUrl: "",
|
||||
};
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.toaster.pop("error", "Failed to load policy");
|
||||
});
|
||||
} else {
|
||||
this.adminTerms.getAllPolicies().then(policies => {
|
||||
this.takenShortcodes = policies.map(p => p.shortcode);
|
||||
this.isLoading = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.toaster.pop("error", "Failed to load policies");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async publish() {
|
||||
this.isUpdating = true;
|
||||
|
||||
await this.adminTerms.updateDraft(this.shortcode, {
|
||||
name: this.languages['en'].name,
|
||||
text: this.languages['en'].text,
|
||||
url: `${window.location.origin}/widgets/terms/${this.shortcode}/en/draft`,
|
||||
});
|
||||
|
||||
|
||||
this.modal.open(AdminTermsNewEditPublishDialogComponent, overlayConfigFactory({
|
||||
isBlocking: true,
|
||||
size: 'sm',
|
||||
}, AdminTermsNewEditPublishDialogContext)).result.then(async (val) => {
|
||||
if (!val) return; // closed without publish
|
||||
|
||||
try {
|
||||
// Change the URL of the draft
|
||||
// TODO: Don't track URLs for drafts
|
||||
await this.adminTerms.updateDraft(this.shortcode, {
|
||||
name: this.languages['en'].name,
|
||||
text: this.languages['en'].text,
|
||||
url: `${window.location.origin}/widgets/terms/${this.shortcode}/en/${val}`,
|
||||
});
|
||||
|
||||
await this.adminTerms.publishDraft(this.shortcode, val);
|
||||
this.toaster.pop("success", "Policy published");
|
||||
this.router.navigate(["../.."], {relativeTo: this.activatedRoute});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.toaster.pop("error", "Error publishing policy");
|
||||
this.isUpdating = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async create() {
|
||||
for (const languageCode in this.languages) {
|
||||
if (this.languages[languageCode].name.trim().length <= 0) {
|
||||
this.toaster.pop("warning", "Please enter a name for all policies");
|
||||
return;
|
||||
}
|
||||
if (this.languages[languageCode].text.trim().length <= 0) {
|
||||
this.toaster.pop("warning", "Please enter text for all policies");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
if (this.isEditing) {
|
||||
try {
|
||||
await this.adminTerms.updateDraft(this.shortcode, {
|
||||
name: this.languages['en'].name,
|
||||
text: this.languages['en'].text,
|
||||
url: `${window.location.origin}/widgets/terms/${this.shortcode}/en/draft`,
|
||||
});
|
||||
|
||||
this.toaster.pop("success", "Draft saved");
|
||||
this.router.navigate(["../.."], {relativeTo: this.activatedRoute});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.toaster.pop("error", "Error saving policy");
|
||||
this.isUpdating = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const startShortcode = this.languages['en'].name.toLowerCase().replace(/[^a-z0-9]/gi, '_');
|
||||
let shortcode = startShortcode;
|
||||
let i = 0;
|
||||
while (this.takenShortcodes.includes(shortcode)) {
|
||||
shortcode = `${startShortcode}_${++i}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.adminTerms.createDraft(shortcode, {
|
||||
name: this.languages['en'].name,
|
||||
text: this.languages['en'].text,
|
||||
url: `${window.location.origin}/widgets/terms/${shortcode}/en/draft`,
|
||||
});
|
||||
|
||||
this.toaster.pop("success", "Draft created");
|
||||
this.router.navigate([".."], {relativeTo: this.activatedRoute});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.toaster.pop("error", "Error creating document");
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
public addLanguage() {
|
||||
this.languages[this.chosenLanguage] = {
|
||||
name: "",
|
||||
text: "",
|
||||
url: "", // TODO: Calculate
|
||||
isExternal: false,
|
||||
externalUrl: "",
|
||||
langName: ISO6391.getName(this.chosenLanguage),
|
||||
};
|
||||
this.chosenLanguage = this.availableLanguages[0].code;
|
||||
}
|
||||
|
||||
}
|
20
web/app/admin/terms/new-edit/publish/publish.component.html
Normal file
20
web/app/admin/terms/new-edit/publish/publish.component.html
Normal file
@ -0,0 +1,20 @@
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<h4>Publish policy</h4>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<label class="label-block">
|
||||
Version number
|
||||
<span class="text-muted ">The version number of this policy</span>
|
||||
<input type="text" class="form-control" placeholder="eg: 1.1.0" [(ngModel)]="version"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button type="button" (click)="publish()" title="close" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-upload"></i> Publish
|
||||
</button>
|
||||
<button type="button" (click)="dialog.close()" title="save" class="btn btn-secondary btn-sm">
|
||||
<i class="far fa-times-circle"></i> Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
button {
|
||||
margin-right: 5px;
|
||||
}
|
27
web/app/admin/terms/new-edit/publish/publish.component.ts
Normal file
27
web/app/admin/terms/new-edit/publish/publish.component.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DialogRef, ModalComponent } from "ngx-modialog";
|
||||
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
|
||||
export class AdminTermsNewEditPublishDialogContext extends BSModalContext {
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./publish.component.html",
|
||||
styleUrls: ["./publish.component.scss"],
|
||||
})
|
||||
export class AdminTermsNewEditPublishDialogComponent implements ModalComponent<AdminTermsNewEditPublishDialogContext> {
|
||||
|
||||
public version: string;
|
||||
|
||||
constructor(public dialog: DialogRef<AdminTermsNewEditPublishDialogContext>, private toaster: ToasterService) {
|
||||
}
|
||||
|
||||
public publish() {
|
||||
if (!this.version || !this.version.trim()) {
|
||||
this.toaster.pop("warning", "Please enter a version number");
|
||||
return;
|
||||
}
|
||||
this.dialog.close(this.version);
|
||||
}
|
||||
}
|
44
web/app/admin/terms/terms.component.html
Normal file
44
web/app/admin/terms/terms.component.html
Normal file
@ -0,0 +1,44 @@
|
||||
<div *ngIf="isLoading">
|
||||
<my-spinner></my-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoading">
|
||||
<my-ibox boxTitle="Terms of Service">
|
||||
<div class="my-ibox-content">
|
||||
<p>
|
||||
Before users can use Dimension they must agree to the terms of service for using your
|
||||
instance. If you're using any matrix.org bridges, users will be required to accept
|
||||
the terms of service for your upstream integration managers (scalar.vector.im usually)
|
||||
in addition to the terms you add here.
|
||||
</p>
|
||||
|
||||
<table class="table table-striped table-condensed table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Policy Name</th>
|
||||
<th>Version</th>
|
||||
<th class="text-center" style="width: 120px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="!policies || policies.length === 0">
|
||||
<td colspan="3"><i>No policies written.</i></td>
|
||||
</tr>
|
||||
<tr *ngFor="let policy of policies trackById">
|
||||
<td>{{ policy.languages['en'].name }}</td>
|
||||
<td>{{ policy.version }}</td>
|
||||
|
||||
<td class="text-center">
|
||||
<span class="editButton" [routerLink]="['edit', policy.shortcode]" title="edit draft"
|
||||
*ngIf="policy.version == 'draft'">
|
||||
<i class="fa fa-pencil-alt"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-success btn-sm" (click)="createPolicy()">
|
||||
<i class="fa fa-plus"></i> New draft policy
|
||||
</button>
|
||||
</div>
|
||||
</my-ibox>
|
||||
</div>
|
3
web/app/admin/terms/terms.component.scss
Normal file
3
web/app/admin/terms/terms.component.scss
Normal file
@ -0,0 +1,3 @@
|
||||
.editButton {
|
||||
cursor: pointer;
|
||||
}
|
42
web/app/admin/terms/terms.component.ts
Normal file
42
web/app/admin/terms/terms.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ToasterService } from "angular2-toaster";
|
||||
import { FE_TermsEditable } from "../../shared/models/terms";
|
||||
import { AdminTermsApiService } from "../../shared/services/admin/admin-terms-api.service";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./terms.component.html",
|
||||
styleUrls: ["./terms.component.scss"],
|
||||
})
|
||||
export class AdminTermsComponent implements OnInit {
|
||||
|
||||
// TODO: "New draft" per policy button
|
||||
// TODO: Delete button
|
||||
|
||||
public isLoading = true;
|
||||
public policies: FE_TermsEditable[];
|
||||
|
||||
constructor(private adminTerms: AdminTermsApiService,
|
||||
private toaster: ToasterService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute) {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.adminTerms.getAllPolicies().then(policies => {
|
||||
this.policies = [
|
||||
...policies.filter(p => p.version === "draft"),
|
||||
...policies.filter(p => p.version !== "draft"),
|
||||
];
|
||||
this.isLoading = false;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.toaster.pop("error", "Failed to load policies");
|
||||
});
|
||||
}
|
||||
|
||||
public createPolicy() {
|
||||
this.router.navigate(["new"], {relativeTo: this.activatedRoute});
|
||||
}
|
||||
|
||||
}
|
@ -20,7 +20,8 @@
|
||||
<td>{{ widget.displayName }}</td>
|
||||
<td>{{ widget.description }}</td>
|
||||
<td class="text-right">
|
||||
<span class="editButton" (click)="editWidget(widget)" *ngIf="widget.isEnabled && hasConfiguration(widget)">
|
||||
<span class="editButton" (click)="editWidget(widget)"
|
||||
*ngIf="widget.isEnabled && hasConfiguration(widget)">
|
||||
<i class="fa fa-pencil-alt"></i>
|
||||
</span>
|
||||
<ui-switch [checked]="widget.isEnabled" size="small" [disabled]="isUpdating"
|
||||
|
@ -112,6 +112,12 @@ import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.se
|
||||
import { AdminLogoutConfirmationDialogComponent } from "./admin/home/logout-confirmation/logout-confirmation.component";
|
||||
import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component";
|
||||
import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component";
|
||||
import { AdminTermsApiService } from "./shared/services/admin/admin-terms-api.service";
|
||||
import { AdminTermsComponent } from "./admin/terms/terms.component";
|
||||
import { CKEditorModule } from "@ckeditor/ckeditor5-angular";
|
||||
import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component";
|
||||
import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component";
|
||||
import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -126,6 +132,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
|
||||
ModalModule.forRoot(),
|
||||
BootstrapModalModule,
|
||||
BreadcrumbsModule,
|
||||
CKEditorModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
@ -204,6 +211,10 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
|
||||
AdminLogoutConfirmationDialogComponent,
|
||||
ReauthExampleWidgetWrapperComponent,
|
||||
ManagerTestWidgetWrapperComponent,
|
||||
AdminTermsComponent,
|
||||
AdminNewEditTermsComponent,
|
||||
AdminTermsNewEditPublishDialogComponent,
|
||||
TermsWidgetWrapperComponent,
|
||||
|
||||
// Vendor
|
||||
],
|
||||
@ -233,6 +244,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
|
||||
SlackApiService,
|
||||
AdminSlackApiService,
|
||||
ToasterService,
|
||||
AdminTermsApiService,
|
||||
{provide: Window, useValue: window},
|
||||
|
||||
// Vendor
|
||||
@ -258,6 +270,7 @@ import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-tes
|
||||
AdminAddCustomBotComponent,
|
||||
AdminSlackBridgeManageSelfhostedComponent,
|
||||
AdminLogoutConfirmationDialogComponent,
|
||||
AdminTermsNewEditPublishDialogComponent,
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
|
@ -44,6 +44,9 @@ import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component
|
||||
import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.component";
|
||||
import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component";
|
||||
import { ManagerTestWidgetWrapperComponent } from "./widget-wrappers/manager-test/manager-test.component";
|
||||
import { AdminTermsComponent } from "./admin/terms/terms.component";
|
||||
import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component";
|
||||
import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: "", component: HomeComponent},
|
||||
@ -145,7 +148,27 @@ const routes: Routes = [
|
||||
component: AdminStickerPacksComponent,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "terms",
|
||||
data: {breadcrumb: "Terms of Service", name: "Terms of Service"},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: AdminTermsComponent,
|
||||
},
|
||||
{
|
||||
path: "new",
|
||||
component: AdminNewEditTermsComponent,
|
||||
data: {breadcrumb: "New policy", name: "New policy"},
|
||||
},
|
||||
{
|
||||
path: "edit/:shortcode",
|
||||
component: AdminNewEditTermsComponent,
|
||||
data: {breadcrumb: "Edit policy", name: "Edit policy"},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -258,6 +281,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "widgets",
|
||||
children: [
|
||||
{path: "terms/:shortcode/:lang/:version", component: TermsWidgetWrapperComponent},
|
||||
{path: "generic", component: GenericWidgetWrapperComponent},
|
||||
{path: "video", component: VideoWidgetWrapperComponent},
|
||||
{path: "jitsi", component: JitsiWidgetWrapperComponent},
|
||||
|
@ -15,7 +15,7 @@
|
||||
<label class="label-block">
|
||||
Gitter Room
|
||||
<input title="room name" type="text" class="form-control form-control-sm col-md-3"
|
||||
[(ngModel)]="gitterRoomName" [disabled]="isBusy" placeholder="my-org/room" />
|
||||
[(ngModel)]="gitterRoomName" [disabled]="isBusy" placeholder="my-org/room"/>
|
||||
</label>
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="bridgeRoom()">
|
||||
Bridge
|
||||
|
@ -25,10 +25,12 @@
|
||||
</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<div class="input-group-addon">#</div>
|
||||
<input title="channel" type="text" class="form-control form-control-sm" [(ngModel)]="channel" [disabled]="loadingOps">
|
||||
<input title="channel" type="text" class="form-control form-control-sm" [(ngModel)]="channel"
|
||||
[disabled]="loadingOps">
|
||||
</div>
|
||||
<div style="margin-top: 25px">
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="loadingOps" (click)="loadOps()">
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="loadingOps"
|
||||
(click)="loadOps()">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
@ -43,7 +45,8 @@
|
||||
</select>
|
||||
</label>
|
||||
<div style="margin-top: 25px">
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="requestingBridge" (click)="requestBridge()">
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="requestingBridge"
|
||||
(click)="requestBridge()">
|
||||
Request Bridge
|
||||
</button>
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div *ngIf="isBridged && bridge.config.link.isWebhook">
|
||||
This room is bridged to Slack using webhooks. Webhook bridging is legacy and doesn't support as
|
||||
rich bridging as the new approach. It is recommended to re-create the bridge with the new process.
|
||||
<br />
|
||||
<br/>
|
||||
<button type="button" class="btn btn-sm btn-danger" [disabled]="isBusy" (click)="unbridgeRoom()">
|
||||
Unbridge
|
||||
</button>
|
||||
@ -28,7 +28,7 @@
|
||||
and channels. Please click the button below to do so.
|
||||
</p>
|
||||
<a [href]="authUrl" rel="noopener" target="_blank">
|
||||
<img src="/img/slack_auth_button.png" class="slack-auth-button" alt="sign in with slack" />
|
||||
<img src="/img/slack_auth_button.png" class="slack-auth-button" alt="sign in with slack"/>
|
||||
</a>
|
||||
</div>
|
||||
<div *ngIf="!isBridged && !needsAuth">
|
||||
|
@ -7,7 +7,8 @@
|
||||
the other room and instead bridge it here?
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
<button type="button" (click)="unbridgeAndContinue()" title="unbridge and continue" class="btn btn-danger btn-sm">
|
||||
<button type="button" (click)="unbridgeAndContinue()" title="unbridge and continue"
|
||||
class="btn btn-danger btn-sm">
|
||||
Unbridge and continue
|
||||
</button>
|
||||
<button type="button" (click)="cancel()" title="cancel" class="btn btn-primary btn-sm">
|
||||
|
@ -8,7 +8,8 @@
|
||||
<div *ngIf="isBridged">
|
||||
This room is bridged to "{{ chatName }}" (<code>{{ chatId }}</code>) on Telegram.
|
||||
<div *ngIf="canUnbridge">
|
||||
<button type="button" class="btn btn-sm btn-danger" [disabled]="isUpdating" (click)="unbridgeRoom()">
|
||||
<button type="button" class="btn btn-sm btn-danger" [disabled]="isUpdating"
|
||||
(click)="unbridgeRoom()">
|
||||
Unbridge
|
||||
</button>
|
||||
</div>
|
||||
@ -20,7 +21,8 @@
|
||||
<label class="label-block">
|
||||
Chat ID
|
||||
<span class="text-muted">After inviting <a href="https://t.me/{{ botUsername }}" target="_blank">@{{ botUsername }}</a> to your Telegram chat, run the command <code>/id</code> in the Telegram room to get the chat ID.</span>
|
||||
<input title="chat ID" type="text" class="form-control form-control-sm col-md-3" [(ngModel)]="chatId" [disabled]="isUpdating" />
|
||||
<input title="chat ID" type="text" class="form-control form-control-sm col-md-3"
|
||||
[(ngModel)]="chatId" [disabled]="isUpdating"/>
|
||||
</label>
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="isUpdating" (click)="bridgeRoom()">
|
||||
Bridge
|
||||
|
@ -7,7 +7,8 @@
|
||||
<div class="my-ibox-content">
|
||||
<label class="label-block">
|
||||
Webhook Name
|
||||
<input title="webhook name" type="text" class="form-control form-control-sm" [(ngModel)]="webhookName" [disabled]="isBusy">
|
||||
<input title="webhook name" type="text" class="form-control form-control-sm"
|
||||
[(ngModel)]="webhookName" [disabled]="isBusy">
|
||||
</label>
|
||||
<div style="margin-top: 25px">
|
||||
<button type="button" class="btn btn-sm btn-primary" [disabled]="isBusy" (click)="newHook()">
|
||||
|
@ -19,11 +19,11 @@
|
||||
<td>{{ feed.url }}</td>
|
||||
<td>{{ feed.addedByUserId }}</td>
|
||||
<td class="actions-col">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
[disabled]="isUpdating || !feed.isSelf"
|
||||
(click)="removeFeed(feed)">
|
||||
<i class="far fa-trash-alt"></i> Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
[disabled]="isUpdating || !feed.isSelf"
|
||||
(click)="removeFeed(feed)">
|
||||
<i class="far fa-trash-alt"></i> Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -33,7 +33,7 @@
|
||||
[(ngModel)]="newFeedUrl"
|
||||
placeholder="https://example.org/feed.atom"
|
||||
name="newFeedUrl"
|
||||
title="New feed URL" />
|
||||
title="New feed URL"/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-outline-success"
|
||||
[disabled]="isUpdating"
|
||||
|
@ -5,7 +5,9 @@
|
||||
.travis.yml configuration and template information
|
||||
</h5>
|
||||
<div class="my-ibox-content">
|
||||
<p>The following section needs to be added to your <code>.travis.yml</code> file in your repositories:</p>
|
||||
<p>
|
||||
The following section needs to be added to your <code>.travis.yml</code> file in your repositories:
|
||||
</p>
|
||||
<pre>{{ travisYaml }}</pre>
|
||||
|
||||
<p>
|
||||
@ -50,15 +52,17 @@
|
||||
<tr *ngFor="let repo of getRepos()">
|
||||
<td>{{ repo.repoKey }}</td>
|
||||
<td>
|
||||
<textarea title="Repository Template" class="repo-template form-control" rows="3" (change)="repo.template = $event.target.value" [disabled]="isUpdating || !repo.isSelf">{{ repo.template }}</textarea>
|
||||
<textarea title="Repository Template" class="repo-template form-control" rows="3"
|
||||
(change)="repo.template = $event.target.value"
|
||||
[disabled]="isUpdating || !repo.isSelf">{{ repo.template }}</textarea>
|
||||
</td>
|
||||
<td>{{ repo.addedByUserId }}</td>
|
||||
<td class="actions-col">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
[disabled]="isUpdating || !repo.isSelf"
|
||||
(click)="removeRepo(repo)">
|
||||
<i class="far fa-trash-alt"></i> Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
[disabled]="isUpdating || !repo.isSelf"
|
||||
(click)="removeRepo(repo)">
|
||||
<i class="far fa-trash-alt"></i> Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -68,7 +72,7 @@
|
||||
[(ngModel)]="newRepoKey"
|
||||
placeholder="matrix-org/synapse"
|
||||
name="newRepoKey"
|
||||
title="New repository name" />
|
||||
title="New repository name"/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-outline-success"
|
||||
[disabled]="isUpdating"
|
||||
|
@ -57,9 +57,9 @@ export class TravisCiComplexBotConfigComponent extends ComplexBotComponent<Travi
|
||||
this.newConfig.repos[this.newRepoKey] = {
|
||||
addedByUserId: SessionStorage.userId,
|
||||
template: "" +
|
||||
"%{repository_slug}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\n" +
|
||||
" Change view : %{compare_url}\n" +
|
||||
" Build details : %{build_url}\n"
|
||||
"%{repository_slug}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\n" +
|
||||
" Change view : %{compare_url}\n" +
|
||||
" Build details : %{build_url}\n"
|
||||
};
|
||||
this.newRepoKey = "";
|
||||
}
|
||||
|
@ -36,7 +36,8 @@
|
||||
<span class="name">{{ pack.displayName }}</span>
|
||||
<span class="description">{{ pack.description }}</span>
|
||||
|
||||
<span class="author" *ngIf="pack.author.type !== 'none'">Created by <a [href]="pack.author.reference">{{ pack.author.name }}</a> under </span>
|
||||
<span class="author" *ngIf="pack.author.type !== 'none'">Created by <a
|
||||
[href]="pack.author.reference">{{ pack.author.name }}</a> under </span>
|
||||
<span class="license"><a [href]="pack.license.urlPath">{{ pack.license.name }}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,8 @@
|
||||
</h5>
|
||||
<div class="my-ibox-content">
|
||||
<form (submit)="widgetComponent.addWidget()" novalidate name="addForm">
|
||||
<ng-container *ngTemplateOutlet="widgetParamsTemplate;context:{widget:widgetComponent.newWidget}"></ng-container>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="widgetParamsTemplate;context:{widget:widgetComponent.newWidget}"></ng-container>
|
||||
|
||||
<div style="margin-top: 25px">
|
||||
<button type="submit" class="btn btn-sm btn-success" [disabled]="widgetComponent.isUpdating">
|
||||
@ -19,7 +20,8 @@
|
||||
</div>
|
||||
</my-ibox>
|
||||
|
||||
<my-ibox *ngFor="let widget of widgetComponent.widgets trackById" [isCollapsible]="true" [defaultCollapsed]="widget.id !== widgetComponent.defaultExpandedWidgetId">
|
||||
<my-ibox *ngFor="let widget of widgetComponent.widgets trackById" [isCollapsible]="true"
|
||||
[defaultCollapsed]="widget.id !== widgetComponent.defaultExpandedWidgetId">
|
||||
<h5 class="my-ibox-title">
|
||||
<i class="fa fa-pencil-alt"></i> {{ widget.name || widget.url || widgetComponent.defaultName }}
|
||||
<span *ngIf="widget.data.title">- {{ widget.data.title }}</span>
|
||||
@ -32,7 +34,8 @@
|
||||
<button type="submit" class="btn btn-sm btn-primary" [disabled]="widgetComponent.isUpdating">
|
||||
<i class="far fa-save"></i> Save Widget
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" [disabled]="widgetComponent.isUpdating" (click)="widgetComponent.removeWidget(widget)">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" [disabled]="widgetComponent.isUpdating"
|
||||
(click)="widgetComponent.removeWidget(widget)">
|
||||
<i class="far fa-trash-alt"></i> Remove Widget
|
||||
</button>
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@ export class GoogleCalendarWidgetConfigComponent extends WidgetComponent {
|
||||
super(WIDGET_GOOGLE_CALENDAR, "Google Calendar", DISABLE_AUTOMATIC_WRAPPING, "googleCalendar");
|
||||
}
|
||||
|
||||
protected OnNewWidgetPrepared(widget: EditableWidget) {
|
||||
protected OnNewWidgetPrepared(widget: EditableWidget) {
|
||||
widget.dimension.newData.shareId = "";
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,8 @@
|
||||
<ng-template #widgetParamsTemplate let-widget="widget">
|
||||
<label class="label-block">
|
||||
Trading Pair
|
||||
<select class="form-control form-control-sm" [(ngModel)]="widget.dimension.newData.pair" [disabled]="isUpdating" name="widget-pair-{{widget.id}}">
|
||||
<select class="form-control form-control-sm" [(ngModel)]="widget.dimension.newData.pair"
|
||||
[disabled]="isUpdating" name="widget-pair-{{widget.id}}">
|
||||
<option *ngFor="let pair of pairs" [ngValue]="pair.value">
|
||||
{{ pair.label }}
|
||||
</option>
|
||||
@ -11,7 +12,8 @@
|
||||
|
||||
<label class="label-block">
|
||||
Interval
|
||||
<select class="form-control form-control-sm" [(ngModel)]="widget.dimension.newData.interval" [disabled]="isUpdating" name="widget-interval-{{widget.id}}">
|
||||
<select class="form-control form-control-sm" [(ngModel)]="widget.dimension.newData.interval"
|
||||
[disabled]="isUpdating" name="widget-interval-{{widget.id}}">
|
||||
<option *ngFor="let interval of intervals" [ngValue]="interval.value">
|
||||
{{ interval.label }}
|
||||
</option>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<div class="ibox">
|
||||
<div class="ibox-title" (click)="isCollapsible ? (collapsed = !collapsed) : false"
|
||||
[ngClass]="[isCollapsible ? 'ibox-title-collapsible' : '']">
|
||||
[ngClass]="[isCollapsible ? 'ibox-title-collapsible' : '']"
|
||||
*ngIf="hasTitle">
|
||||
<h5 *ngIf="boxTitle">
|
||||
{{ boxTitle }}
|
||||
</h5>
|
||||
|
@ -11,6 +11,7 @@ export class IboxComponent implements OnInit {
|
||||
@Input() boxTitle: string;
|
||||
@Input() isCollapsible: boolean;
|
||||
@Input() defaultCollapsed: boolean;
|
||||
@Input() hasTitle = true;
|
||||
|
||||
public collapsed = false;
|
||||
|
||||
|
@ -37,7 +37,8 @@
|
||||
<div *ngFor="let category of getCategories()">
|
||||
<my-ibox *ngIf="getIntegrationsIn(category).length > 0" boxTitle="{{category}}" [isCollapsible]="true">
|
||||
<div class="my-ibox-content">
|
||||
<my-integration-bag [integrations]="getIntegrationsIn(category)" (integrationClicked)="modifyIntegration($event)"></my-integration-bag>
|
||||
<my-integration-bag [integrations]="getIntegrationsIn(category)"
|
||||
(integrationClicked)="modifyIntegration($event)"></my-integration-bag>
|
||||
</div>
|
||||
</my-ibox>
|
||||
</div>
|
||||
|
@ -2,7 +2,21 @@ import { FE_Integration } from "./models/integration";
|
||||
|
||||
export class SessionStorage {
|
||||
|
||||
public static scalarToken: string;
|
||||
private static _scalarToken: string;
|
||||
|
||||
public static get scalarToken(): string {
|
||||
if (this._scalarToken) return this._scalarToken;
|
||||
this.scalarToken = localStorage.getItem("dimension_scalar_token");
|
||||
return this._scalarToken;
|
||||
}
|
||||
|
||||
public static set scalarToken(val: string) {
|
||||
this._scalarToken = val;
|
||||
if (val) {
|
||||
localStorage.setItem("dimension_scalar_token", val);
|
||||
}
|
||||
}
|
||||
|
||||
public static userId: string;
|
||||
public static roomId: string;
|
||||
public static isAdmin: boolean;
|
||||
|
16
web/app/shared/models/terms.ts
Normal file
16
web/app/shared/models/terms.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface FE_TermsEditable {
|
||||
shortcode: string;
|
||||
version: string;
|
||||
languages: {
|
||||
[lang: string]: {
|
||||
name: string;
|
||||
url: string;
|
||||
text?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface FE_MinimalTerms {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
@ -5,7 +5,9 @@ import {
|
||||
WIDGET_GOOGLE_CALENDAR,
|
||||
WIDGET_GOOGLE_DOCS,
|
||||
WIDGET_GRAFANA,
|
||||
WIDGET_JITSI, WIDGET_SPOTIFY, WIDGET_STICKER_PICKER,
|
||||
WIDGET_JITSI,
|
||||
WIDGET_SPOTIFY,
|
||||
WIDGET_STICKER_PICKER,
|
||||
WIDGET_TRADINGVIEW,
|
||||
WIDGET_TWITCH,
|
||||
WIDGET_YOUTUBE
|
||||
|
31
web/app/shared/services/admin/admin-terms-api.service.ts
Normal file
31
web/app/shared/services/admin/admin-terms-api.service.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { AuthedApi } from "../authed-api";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { FE_TermsEditable } from "../../models/terms";
|
||||
|
||||
@Injectable()
|
||||
export class AdminTermsApiService extends AuthedApi {
|
||||
constructor(http: HttpClient) {
|
||||
super(http, true);
|
||||
}
|
||||
|
||||
public getAllPolicies(): Promise<FE_TermsEditable[]> {
|
||||
return this.authedGet<FE_TermsEditable[]>("/api/v1/dimension/admin/terms/all").toPromise();
|
||||
}
|
||||
|
||||
public createDraft(shortcode: string, policyInfo: { name: string, text: string, url: string }): Promise<FE_TermsEditable> {
|
||||
return this.authedPost<FE_TermsEditable>(`/api/v1/dimension/admin/terms/${shortcode}/draft`, policyInfo).toPromise();
|
||||
}
|
||||
|
||||
public getDraft(shortcode: string): Promise<FE_TermsEditable> {
|
||||
return this.authedGet<FE_TermsEditable>(`/api/v1/dimension/admin/terms/${shortcode}/draft`).toPromise();
|
||||
}
|
||||
|
||||
public updateDraft(shortcode: string, policyInfo: { name: string, text: string, url: string }): Promise<FE_TermsEditable> {
|
||||
return this.authedPut<FE_TermsEditable>(`/api/v1/dimension/admin/terms/${shortcode}/draft`, policyInfo).toPromise();
|
||||
}
|
||||
|
||||
public publishDraft(shortcode: string, newVersion: string): Promise<FE_TermsEditable> {
|
||||
return this.authedPost<FE_TermsEditable>(`/api/v1/dimension/admin/terms/${shortcode}/publish/${newVersion}`).toPromise();
|
||||
}
|
||||
}
|
@ -3,24 +3,40 @@ import { SessionStorage } from "../SessionStorage";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
|
||||
export class AuthedApi {
|
||||
constructor(protected http: HttpClient) {
|
||||
constructor(protected http: HttpClient, private matrixAuth = false) {
|
||||
}
|
||||
|
||||
protected authedGet<T>(url: string, qs?: any): Observable<T> {
|
||||
if (!qs) qs = {};
|
||||
qs["scalar_token"] = SessionStorage.scalarToken;
|
||||
return this.http.get<T>(url, {params: qs});
|
||||
const opts = this.fillAuthOptions(null, qs, null);
|
||||
return this.http.get<T>(url, opts);
|
||||
}
|
||||
|
||||
protected authedPost<T>(url: string, body?: any): Observable<T> {
|
||||
if (!body) body = {};
|
||||
const qs = {scalar_token: SessionStorage.scalarToken};
|
||||
return this.http.post<T>(url, body, {params: qs});
|
||||
const opts = this.fillAuthOptions(null, null, null);
|
||||
return this.http.post<T>(url, body, opts);
|
||||
}
|
||||
|
||||
protected authedPut<T>(url: string, body?: any): Observable<T> {
|
||||
if (!body) body = {};
|
||||
const opts = this.fillAuthOptions(null, null, null);
|
||||
return this.http.put<T>(url, body, opts);
|
||||
}
|
||||
|
||||
protected authedDelete<T>(url: string, qs?: any): Observable<T> {
|
||||
const opts = this.fillAuthOptions(null, qs, null);
|
||||
return this.http.delete<T>(url, opts);
|
||||
}
|
||||
|
||||
private fillAuthOptions(opts: any, qs: any, headers: any): { headers: any, params: any } {
|
||||
if (!opts) opts = {};
|
||||
if (!qs) qs = {};
|
||||
qs["scalar_token"] = SessionStorage.scalarToken;
|
||||
return this.http.delete<T>(url, {params: qs});
|
||||
if (!headers) headers = {};
|
||||
if (this.matrixAuth) {
|
||||
headers["Authorization"] = `Bearer ${SessionStorage.scalarToken}`;
|
||||
} else {
|
||||
qs["scalar_token"] = SessionStorage.scalarToken;
|
||||
}
|
||||
return Object.assign({}, opts, {params: qs, headers: headers});
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { AuthedApi } from "../authed-api";
|
||||
import { FE_Widget } from "../../models/integration";
|
||||
import { IntegrationsApiService } from "./integrations-api.service";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { FE_MinimalTerms } from "../../models/terms";
|
||||
|
||||
@Injectable()
|
||||
export class WidgetApiService extends AuthedApi {
|
||||
@ -17,4 +18,8 @@ export class WidgetApiService extends AuthedApi {
|
||||
public isEmbeddable(url: string): Promise<any> { // 200 = success, anything else = error
|
||||
return this.http.get("/api/v1/dimension/widgets/embeddable", {params: {url: url}}).toPromise();
|
||||
}
|
||||
|
||||
public getTerms(shortcode: string, language: string, version: string): Promise<FE_MinimalTerms> {
|
||||
return this.http.get<FE_MinimalTerms>(`/api/v1/dimension/widgets/terms/${shortcode}/${language}/${version}`).toPromise();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import {
|
||||
JoinRuleStateResponse,
|
||||
MembershipStateResponse,
|
||||
RoomEncryptionStatusResponse,
|
||||
ScalarSuccessResponse, ScalarWidget,
|
||||
ScalarSuccessResponse,
|
||||
ScalarWidget,
|
||||
SetPowerLevelResponse,
|
||||
WidgetsResponse
|
||||
} from "../../models/server-client-responses";
|
||||
@ -73,7 +74,7 @@ export class ScalarClientApiService {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteWidget(roomId: string, widget: EditableWidget|ScalarWidget): Promise<ScalarSuccessResponse> {
|
||||
public deleteWidget(roomId: string, widget: EditableWidget | ScalarWidget): Promise<ScalarSuccessResponse> {
|
||||
const anyWidget: any = widget;
|
||||
return this.callAction("set_widget", {
|
||||
room_id: roomId,
|
||||
@ -83,7 +84,7 @@ export class ScalarClientApiService {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteUserWidget(widget: EditableWidget|ScalarWidget): Promise<ScalarSuccessResponse> {
|
||||
public deleteUserWidget(widget: EditableWidget | ScalarWidget): Promise<ScalarSuccessResponse> {
|
||||
const anyWidget: any = widget;
|
||||
return this.callAction("set_widget", {
|
||||
userWidget: true,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user