Merge branch 'travis/msc2140-tos'

This commit is contained in:
Travis Ralston 2019-07-22 18:09:41 -06:00
commit dd53cb8484
111 changed files with 2855 additions and 625 deletions

30
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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";

View File

@ -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;
}
}

View File

@ -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();
});
}

View File

@ -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();

View File

@ -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};
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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();

View 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,
};
}
}

View File

@ -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);

View File

@ -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") {

View File

@ -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");

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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> {

View File

@ -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 {};

View 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 {};
}
}

View File

@ -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

View File

@ -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;

View File

@ -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 {
}
}

View 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;
}
}

View File

@ -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,
]);
}

View File

@ -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() {
}
}

View File

@ -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) => {

View File

@ -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
},
]);
});
},

View 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"));
}
}

View 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"));
}
}

View 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"));
}
}

View 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[];
}

View 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;
}

View 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;
}

View 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;
}

View File

@ -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;
}

View File

@ -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
}

View File

@ -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);
}
});

View File

@ -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
View 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();
}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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()">

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>) {
}
}

View File

@ -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

View File

@ -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>

View File

@ -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) }}

View File

@ -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>

View 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>

View File

@ -0,0 +1,8 @@
.buttons button {
margin-right: 5px;
}
.buttons select {
max-width: 200px;
margin-bottom: 5px;
}

View 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;
}
}

View 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>

View File

@ -0,0 +1,3 @@
button {
margin-right: 5px;
}

View 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);
}
}

View 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>

View File

@ -0,0 +1,3 @@
.editButton {
cursor: pointer;
}

View 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});
}
}

View File

@ -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"

View File

@ -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 {

View File

@ -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},

View File

@ -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

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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

View File

@ -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()">

View File

@ -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"

View File

@ -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"

View File

@ -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 = "";
}

View File

@ -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>

View File

@ -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>

View File

@ -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 = "";
}

View File

@ -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>

View File

@ -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>

View File

@ -11,6 +11,7 @@ export class IboxComponent implements OnInit {
@Input() boxTitle: string;
@Input() isCollapsible: boolean;
@Input() defaultCollapsed: boolean;
@Input() hasTitle = true;
public collapsed = false;

View File

@ -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>

View File

@ -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;

View 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;
}

View File

@ -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

View 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();
}
}

View File

@ -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});
}
}

View File

@ -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();
}
}

View File

@ -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