mirror of
https://github.com/turt2live/matrix-dimension.git
synced 2024-07-01 00:31:23 +00:00
Implement MSC1961
See https://github.com/matrix-org/matrix-doc/pull/1961
This commit is contained in:
parent
d021974a22
commit
57d585d68a
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -11705,6 +11705,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.3.tgz",
|
||||||
"integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ=="
|
"integrity": "sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ=="
|
||||||
},
|
},
|
||||||
|
"typescript-ioc": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript-ioc/-/typescript-ioc-1.2.5.tgz",
|
||||||
|
"integrity": "sha512-HErBOZfOmrJ9N8QZDOHvP56FqvqZJMkaFW9Qm4ExQa93ilUnhQ+S7n80rVfUQPceZWIImsEBU/Kt19W5KXBDEw==",
|
||||||
|
"requires": {
|
||||||
|
"reflect-metadata": "^0.1.10",
|
||||||
|
"require-glob": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"typescript-rest": {
|
"typescript-rest": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-rest/-/typescript-rest-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-rest/-/typescript-rest-2.0.0.tgz",
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
"sqlite3": "^4.0.6",
|
"sqlite3": "^4.0.6",
|
||||||
"telegraf": "^3.28.0",
|
"telegraf": "^3.28.0",
|
||||||
"typescript": "^3.4.3",
|
"typescript": "^3.4.3",
|
||||||
|
"typescript-ioc": "^1.2.5",
|
||||||
"typescript-rest": "^2.0.0",
|
"typescript-rest": "^2.0.0",
|
||||||
"umzug": "^2.2.0",
|
"umzug": "^2.2.0",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Server } from "typescript-rest";
|
||||||
import * as _ from "lodash";
|
import * as _ from "lodash";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import { ApiError } from "./ApiError";
|
import { ApiError } from "./ApiError";
|
||||||
|
import MSCSecurity from "./security/MSCSecurity";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Web server for Dimension. Handles the API routes for the admin, scalar, dimension, and matrix APIs.
|
* Web server for Dimension. Handles the API routes for the admin, scalar, dimension, and matrix APIs.
|
||||||
|
@ -23,8 +24,12 @@ export default class Webserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadRoutes() {
|
private loadRoutes() {
|
||||||
const apis = ["scalar", "dimension", "admin", "matrix"].map(a => path.join(__dirname, a, "*.js"));
|
// 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 router = express.Router();
|
const router = express.Router();
|
||||||
|
Server.useIoC();
|
||||||
|
Server.registerAuthenticator(new MSCSecurity());
|
||||||
apis.forEach(a => Server.loadServices(router, [a]));
|
apis.forEach(a => Server.loadServices(router, [a]));
|
||||||
const routes = _.uniq(router.stack.map(r => r.route.path));
|
const routes = _.uniq(router.stack.map(r => r.route.path));
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
||||||
|
@ -7,6 +6,7 @@ import { CURRENT_VERSION } from "../../version";
|
||||||
import { getFederationConnInfo } from "../../matrix/helpers";
|
import { getFederationConnInfo } from "../../matrix/helpers";
|
||||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
interface DimensionVersionResponse {
|
interface DimensionVersionResponse {
|
||||||
version: string;
|
version: string;
|
||||||
|
@ -50,7 +50,8 @@ export class AdminService {
|
||||||
* @throws {ApiError} Thrown with a status code of 401 if the owner is not 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> {
|
public static async validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken, true);
|
const accountController = new AccountController();
|
||||||
|
const userId = await accountController.getTokenOwner(scalarToken, true);
|
||||||
if (!AdminService.isAdmin(userId))
|
if (!AdminService.isAdmin(userId))
|
||||||
throw new ApiError(401, "You must be an administrator to use this API");
|
throw new ApiError(401, "You must be an administrator to use this API");
|
||||||
return userId;
|
return userId;
|
||||||
|
|
123
src/api/controllers/AccountController.ts
Normal file
123
src/api/controllers/AccountController.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { OpenId } from "../../models/OpenId";
|
||||||
|
import { MatrixOpenIdClient } from "../../matrix/MatrixOpenIdClient";
|
||||||
|
import { LogService } from "matrix-js-snippets";
|
||||||
|
import { ApiError } from "../ApiError";
|
||||||
|
import User from "../../db/models/User";
|
||||||
|
import Upstream from "../../db/models/Upstream";
|
||||||
|
import { ScalarStore } from "../../db/ScalarStore";
|
||||||
|
import UserScalarToken from "../../db/models/UserScalarToken";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export interface IAccountRegisteredResponse {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountInfoResponse {
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API controller for account management
|
||||||
|
*/
|
||||||
|
@AutoWired
|
||||||
|
export default class AccountController {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
||||||
|
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
|
||||||
|
if (cachedUserId) return cachedUserId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
|
||||||
|
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
|
||||||
|
return user.userId;
|
||||||
|
} catch (err) {
|
||||||
|
LogService.error("ScalarService", err);
|
||||||
|
throw new ApiError(401, "Invalid token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an account to use the Integration Manager
|
||||||
|
* @param {OpenId} openId The OpenID request information.
|
||||||
|
* @returns {Promise<IAccountRegisteredResponse>} Resolves when registered.
|
||||||
|
*/
|
||||||
|
public async registerAccount(openId: OpenId): Promise<IAccountRegisteredResponse> {
|
||||||
|
if (!openId || !openId.matrix_server_name || !openId.access_token) {
|
||||||
|
throw new ApiError(400, "Missing OpenID information");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mxClient = new MatrixOpenIdClient(openId);
|
||||||
|
const mxUserId = await mxClient.getUserId();
|
||||||
|
|
||||||
|
if (!mxUserId.endsWith(":" + openId.matrix_server_name)) {
|
||||||
|
LogService.warn("AccountController", `OpenID subject '${mxUserId}' does not belong to the homeserver '${openId.matrix_server_name}'`);
|
||||||
|
throw new ApiError(401, "Invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findByPrimary(mxUserId);
|
||||||
|
if (!user) {
|
||||||
|
// There's a small chance we'll get a validation error because of:
|
||||||
|
// https://github.com/vector-im/riot-web/issues/5846
|
||||||
|
LogService.verbose("AccountController", "User " + mxUserId + " never seen before - creating");
|
||||||
|
await User.create({userId: mxUserId});
|
||||||
|
}
|
||||||
|
|
||||||
|
const upstreams = await Upstream.findAll();
|
||||||
|
await Promise.all(upstreams.map(async upstream => {
|
||||||
|
if (!await ScalarStore.isUpstreamOnline(upstream)) {
|
||||||
|
LogService.warn("AccountController", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tokens = await UserScalarToken.findAll({where: {userId: mxUserId, upstreamId: upstream.id}});
|
||||||
|
if (!tokens || tokens.length === 0) {
|
||||||
|
LogService.info("AccountController", "Registering " + mxUserId + " for a token at upstream " + upstream.id + " (" + upstream.name + ")");
|
||||||
|
const client = new ScalarClient(upstream);
|
||||||
|
const response = await client.register(openId);
|
||||||
|
return UserScalarToken.create({
|
||||||
|
userId: mxUserId,
|
||||||
|
scalarToken: response.scalar_token,
|
||||||
|
isDimensionToken: false,
|
||||||
|
upstreamId: upstream.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).filter(token => !!token));
|
||||||
|
|
||||||
|
const dimensionToken = randomString({length: 25});
|
||||||
|
const dimensionScalarToken = await UserScalarToken.create({
|
||||||
|
userId: mxUserId,
|
||||||
|
scalarToken: dimensionToken,
|
||||||
|
isDimensionToken: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogService.info("AccountController", mxUserId + " has registered for a scalar token successfully");
|
||||||
|
return {token: dimensionScalarToken.scalarToken};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a user out
|
||||||
|
* @param {IMSCUser} 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}});
|
||||||
|
for (const token of tokens) {
|
||||||
|
await token.destroy();
|
||||||
|
}
|
||||||
|
Cache.for(CACHE_SCALAR_ACCOUNTS).clear();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge";
|
import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
interface BridgeRoomRequest {
|
interface BridgeRoomRequest {
|
||||||
gitterRoomName: string;
|
gitterRoomName: string;
|
||||||
|
@ -12,12 +13,16 @@ interface BridgeRoomRequest {
|
||||||
* API for interacting with the Gitter bridge
|
* API for interacting with the Gitter bridge
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/gitter")
|
@Path("/api/v1/dimension/gitter")
|
||||||
|
@AutoWired
|
||||||
export class DimensionGitterService {
|
export class DimensionGitterService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("room/:roomId/link")
|
@Path("room/:roomId/link")
|
||||||
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedRoom> {
|
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedRoom> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gitter = new GitterBridge(userId);
|
const gitter = new GitterBridge(userId);
|
||||||
|
@ -31,7 +36,7 @@ export class DimensionGitterService {
|
||||||
@POST
|
@POST
|
||||||
@Path("room/:roomId/link")
|
@Path("room/:roomId/link")
|
||||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
|
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gitter = new GitterBridge(userId);
|
const gitter = new GitterBridge(userId);
|
||||||
|
@ -46,7 +51,7 @@ export class DimensionGitterService {
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("room/:roomId/link")
|
@Path("room/:roomId/link")
|
||||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gitter = new GitterBridge(userId);
|
const gitter = new GitterBridge(userId);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import { Widget } from "../../integrations/Widget";
|
import { Widget } from "../../integrations/Widget";
|
||||||
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
|
||||||
import { Integration } from "../../integrations/Integration";
|
import { Integration } from "../../integrations/Integration";
|
||||||
|
@ -11,6 +10,8 @@ import { ComplexBot } from "../../integrations/ComplexBot";
|
||||||
import { Bridge } from "../../integrations/Bridge";
|
import { Bridge } from "../../integrations/Bridge";
|
||||||
import { BridgeStore } from "../../db/BridgeStore";
|
import { BridgeStore } from "../../db/BridgeStore";
|
||||||
import { BotStore } from "../../db/BotStore";
|
import { BotStore } from "../../db/BotStore";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
|
||||||
export interface IntegrationsResponse {
|
export interface IntegrationsResponse {
|
||||||
widgets: Widget[],
|
widgets: Widget[],
|
||||||
|
@ -23,8 +24,12 @@ export interface IntegrationsResponse {
|
||||||
* API for managing integrations, primarily for a given room
|
* API for managing integrations, primarily for a given room
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/integrations")
|
@Path("/api/v1/dimension/integrations")
|
||||||
|
@AutoWired
|
||||||
export class DimensionIntegrationsService {
|
export class DimensionIntegrationsService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of widgets
|
* Gets a list of widgets
|
||||||
* @param {boolean} enabledOnly True to only return the enabled widgets
|
* @param {boolean} enabledOnly True to only return the enabled widgets
|
||||||
|
@ -86,7 +91,7 @@ export class DimensionIntegrationsService {
|
||||||
@GET
|
@GET
|
||||||
@Path("room/:roomId")
|
@Path("room/:roomId")
|
||||||
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
|
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
return {
|
return {
|
||||||
widgets: await DimensionIntegrationsService.getWidgets(true),
|
widgets: await DimensionIntegrationsService.getWidgets(true),
|
||||||
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
bots: await DimensionIntegrationsService.getSimpleBots(userId),
|
||||||
|
@ -110,7 +115,7 @@ export class DimensionIntegrationsService {
|
||||||
@POST
|
@POST
|
||||||
@Path("room/:roomId/integrations/:category/:type/config")
|
@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> {
|
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 ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
|
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
|
||||||
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
|
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
|
||||||
|
@ -123,7 +128,7 @@ export class DimensionIntegrationsService {
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("room/:roomId/integrations/:category/:type")
|
@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> {
|
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 ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
|
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
|
||||||
else if (category === "bot") {
|
else if (category === "bot") {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import { IrcBridge } from "../../bridges/IrcBridge";
|
import { IrcBridge } from "../../bridges/IrcBridge";
|
||||||
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
|
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
|
||||||
interface RequestLinkRequest {
|
interface RequestLinkRequest {
|
||||||
op: string;
|
op: string;
|
||||||
|
@ -13,12 +14,16 @@ interface RequestLinkRequest {
|
||||||
* API for interacting with the IRC bridge
|
* API for interacting with the IRC bridge
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/irc")
|
@Path("/api/v1/dimension/irc")
|
||||||
|
@AutoWired
|
||||||
export class DimensionIrcService {
|
export class DimensionIrcService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path(":networkId/channel/:channel/ops")
|
@Path(":networkId/channel/:channel/ops")
|
||||||
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
|
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||||
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||||
|
@ -34,7 +39,7 @@ export class DimensionIrcService {
|
||||||
@POST
|
@POST
|
||||||
@Path(":networkId/channel/:channel/link/:roomId")
|
@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> {
|
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 ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||||
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||||
|
@ -50,7 +55,7 @@ export class DimensionIrcService {
|
||||||
@POST
|
@POST
|
||||||
@Path(":networkId/channel/:channel/unlink/:roomId")
|
@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> {
|
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 ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const parsed = IrcBridge.parseNetworkId(networkId);
|
const parsed = IrcBridge.parseNetworkId(networkId);
|
||||||
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
const bridge = await IrcBridgeRecord.findByPrimary(parsed.bridgeId);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge";
|
import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge";
|
||||||
import { SlackChannel, SlackTeam } from "../../bridges/models/slack";
|
import { SlackChannel, SlackTeam } from "../../bridges/models/slack";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
interface BridgeRoomRequest {
|
interface BridgeRoomRequest {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
|
@ -14,12 +15,16 @@ interface BridgeRoomRequest {
|
||||||
* API for interacting with the Slack bridge
|
* API for interacting with the Slack bridge
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/slack")
|
@Path("/api/v1/dimension/slack")
|
||||||
|
@AutoWired
|
||||||
export class DimensionSlackService {
|
export class DimensionSlackService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("room/:roomId/link")
|
@Path("room/:roomId/link")
|
||||||
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedChannel> {
|
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedChannel> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slack = new SlackBridge(userId);
|
const slack = new SlackBridge(userId);
|
||||||
|
@ -33,7 +38,7 @@ export class DimensionSlackService {
|
||||||
@POST
|
@POST
|
||||||
@Path("room/:roomId/link")
|
@Path("room/:roomId/link")
|
||||||
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
|
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slack = new SlackBridge(userId);
|
const slack = new SlackBridge(userId);
|
||||||
|
@ -48,7 +53,7 @@ export class DimensionSlackService {
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("room/:roomId/link")
|
@Path("room/:roomId/link")
|
||||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slack = new SlackBridge(userId);
|
const slack = new SlackBridge(userId);
|
||||||
|
@ -65,7 +70,7 @@ export class DimensionSlackService {
|
||||||
@GET
|
@GET
|
||||||
@Path("teams")
|
@Path("teams")
|
||||||
public async getTeams(@QueryParam("scalar_token") scalarToken: string): Promise<SlackTeam[]> {
|
public async getTeams(@QueryParam("scalar_token") scalarToken: string): Promise<SlackTeam[]> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const slack = new SlackBridge(userId);
|
const slack = new SlackBridge(userId);
|
||||||
const teams = await slack.getTeams();
|
const teams = await slack.getTeams();
|
||||||
|
@ -76,7 +81,7 @@ export class DimensionSlackService {
|
||||||
@GET
|
@GET
|
||||||
@Path("teams/:teamId/channels")
|
@Path("teams/:teamId/channels")
|
||||||
public async getChannels(@QueryParam("scalar_token") scalarToken: string, @PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
|
public async getChannels(@QueryParam("scalar_token") scalarToken: string, @PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slack = new SlackBridge(userId);
|
const slack = new SlackBridge(userId);
|
||||||
|
@ -90,7 +95,7 @@ export class DimensionSlackService {
|
||||||
@GET
|
@GET
|
||||||
@Path("auth")
|
@Path("auth")
|
||||||
public async getAuthUrl(@QueryParam("scalar_token") scalarToken: string): Promise<{ authUrl: string }> {
|
public async getAuthUrl(@QueryParam("scalar_token") scalarToken: string): Promise<{ authUrl: string }> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slack = new SlackBridge(userId);
|
const slack = new SlackBridge(userId);
|
||||||
|
|
|
@ -2,12 +2,13 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
|
import { Cache, CACHE_STICKERS } from "../../MemoryCache";
|
||||||
import StickerPack from "../../db/models/StickerPack";
|
import StickerPack from "../../db/models/StickerPack";
|
||||||
import Sticker from "../../db/models/Sticker";
|
import Sticker from "../../db/models/Sticker";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import UserStickerPack from "../../db/models/UserStickerPack";
|
import UserStickerPack from "../../db/models/UserStickerPack";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
|
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
|
||||||
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
|
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
|
||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
export interface MemoryStickerPack {
|
export interface MemoryStickerPack {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -63,8 +64,12 @@ interface StickerConfig {
|
||||||
* API for stickers
|
* API for stickers
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/stickers")
|
@Path("/api/v1/dimension/stickers")
|
||||||
|
@AutoWired
|
||||||
export class DimensionStickerService {
|
export class DimensionStickerService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> {
|
public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> {
|
||||||
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
|
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
|
||||||
if (cachedPacks) {
|
if (cachedPacks) {
|
||||||
|
@ -86,7 +91,7 @@ export class DimensionStickerService {
|
||||||
@GET
|
@GET
|
||||||
@Path("config")
|
@Path("config")
|
||||||
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<StickerConfig> {
|
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<StickerConfig> {
|
||||||
await ScalarService.getTokenOwner(scalarToken);
|
await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: config.stickers.enabled,
|
enabled: config.stickers.enabled,
|
||||||
|
@ -98,7 +103,7 @@ export class DimensionStickerService {
|
||||||
@GET
|
@GET
|
||||||
@Path("packs")
|
@Path("packs")
|
||||||
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
|
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
|
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
|
||||||
if (cachedPacks) return cachedPacks;
|
if (cachedPacks) return cachedPacks;
|
||||||
|
@ -125,7 +130,7 @@ export class DimensionStickerService {
|
||||||
@POST
|
@POST
|
||||||
@Path("packs/:packId/selected")
|
@Path("packs/:packId/selected")
|
||||||
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
|
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const pack = await StickerPack.findByPrimary(packId);
|
const pack = await StickerPack.findByPrimary(packId);
|
||||||
if (!pack) throw new ApiError(404, "Sticker pack not found");
|
if (!pack) throw new ApiError(404, "Sticker pack not found");
|
||||||
|
@ -149,7 +154,7 @@ export class DimensionStickerService {
|
||||||
@POST
|
@POST
|
||||||
@Path("packs/import")
|
@Path("packs/import")
|
||||||
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> {
|
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> {
|
||||||
await ScalarService.getTokenOwner(scalarToken);
|
await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
if (!config.stickers.enabled) {
|
if (!config.stickers.enabled) {
|
||||||
throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver");
|
throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver");
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import { TelegramBridge } from "../../bridges/TelegramBridge";
|
import { TelegramBridge } from "../../bridges/TelegramBridge";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
interface PortalInfoResponse {
|
interface PortalInfoResponse {
|
||||||
bridged: boolean;
|
bridged: boolean;
|
||||||
|
@ -19,12 +20,16 @@ interface BridgeRoomRequest {
|
||||||
* API for interacting with the Telegram bridge
|
* API for interacting with the Telegram bridge
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/telegram")
|
@Path("/api/v1/dimension/telegram")
|
||||||
|
@AutoWired
|
||||||
export class DimensionTelegramService {
|
export class DimensionTelegramService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("chat/:chatId")
|
@Path("chat/:chatId")
|
||||||
public async getPortalInfo(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
public async getPortalInfo(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegram = new TelegramBridge(userId);
|
const telegram = new TelegramBridge(userId);
|
||||||
|
@ -47,7 +52,7 @@ export class DimensionTelegramService {
|
||||||
@POST
|
@POST
|
||||||
@Path("chat/:chatId/room/:roomId")
|
@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> {
|
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegram = new TelegramBridge(userId);
|
const telegram = new TelegramBridge(userId);
|
||||||
|
@ -69,7 +74,7 @@ export class DimensionTelegramService {
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("room/:roomId")
|
@Path("room/:roomId")
|
||||||
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const telegram = new TelegramBridge(userId);
|
const telegram = new TelegramBridge(userId);
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
import { DELETE, FormParam, HeaderParam, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
import { DELETE, FormParam, HeaderParam, Path, PathParam, POST, QueryParam } from "typescript-rest";
|
||||||
import { ScalarService } from "../scalar/ScalarService";
|
|
||||||
import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks";
|
import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks";
|
||||||
import { WebhooksBridge } from "../../bridges/WebhooksBridge";
|
import { WebhooksBridge } from "../../bridges/WebhooksBridge";
|
||||||
import Webhook from "../../db/models/Webhook";
|
import Webhook from "../../db/models/Webhook";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
import * as request from "request";
|
import * as request from "request";
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API for interacting with the Webhooks bridge, and for setting up proxies to other
|
* API for interacting with the Webhooks bridge, and for setting up proxies to other
|
||||||
* services.
|
* services.
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/dimension/webhooks")
|
@Path("/api/v1/dimension/webhooks")
|
||||||
|
@AutoWired
|
||||||
export class DimensionWebhooksService {
|
export class DimensionWebhooksService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/travisci/:webhookId")
|
@Path("/travisci/:webhookId")
|
||||||
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
|
public async postTravisCiWebhook(@PathParam("webhookId") webhookId: string, @FormParam("payload") payload: string, @HeaderParam("Signature") signature: string): Promise<any> {
|
||||||
|
@ -43,7 +48,7 @@ export class DimensionWebhooksService {
|
||||||
@POST
|
@POST
|
||||||
@Path("room/:roomId/webhooks/new")
|
@Path("room/:roomId/webhooks/new")
|
||||||
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const webhooks = new WebhooksBridge(userId);
|
const webhooks = new WebhooksBridge(userId);
|
||||||
return webhooks.createWebhook(roomId, options);
|
return webhooks.createWebhook(roomId, options);
|
||||||
|
@ -52,7 +57,7 @@ export class DimensionWebhooksService {
|
||||||
@POST
|
@POST
|
||||||
@Path("room/:roomId/webhooks/:hookId")
|
@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> {
|
public async updateWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const webhooks = new WebhooksBridge(userId);
|
const webhooks = new WebhooksBridge(userId);
|
||||||
return webhooks.updateWebhook(roomId, hookId, options);
|
return webhooks.updateWebhook(roomId, hookId, options);
|
||||||
|
@ -61,7 +66,7 @@ export class DimensionWebhooksService {
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("room/:roomId/webhooks/:hookId")
|
@Path("room/:roomId/webhooks/:hookId")
|
||||||
public async deleteWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
|
public async deleteWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const webhooks = new WebhooksBridge(userId);
|
const webhooks = new WebhooksBridge(userId);
|
||||||
return webhooks.deleteWebhook(roomId, hookId);
|
return webhooks.deleteWebhook(roomId, hookId);
|
||||||
|
|
41
src/api/msc/MSCAccountService.ts
Normal file
41
src/api/msc/MSCAccountService.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest";
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API for account management
|
||||||
|
*/
|
||||||
|
@Path("/_matrix/integrations/v1/account")
|
||||||
|
@AutoWired
|
||||||
|
export class MSCAccountService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private context: ServiceContext;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("register")
|
||||||
|
public async register(request: OpenId): Promise<IAccountRegisteredResponse> {
|
||||||
|
return this.accountController.registerAccount(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("")
|
||||||
|
@Security(ROLE_MSC_USER)
|
||||||
|
public async info(): Promise<IAccountInfoResponse> {
|
||||||
|
const user: IMSCUser = this.context.request.user;
|
||||||
|
return {user_id: user.userId};
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("logout")
|
||||||
|
@Security(ROLE_MSC_USER)
|
||||||
|
public async logout(): Promise<any> {
|
||||||
|
await this.accountController.logout(this.context.request.user);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,104 +1,30 @@
|
||||||
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
import { GET, Path, POST, QueryParam } from "typescript-rest";
|
||||||
import { MatrixOpenIdClient } from "../../matrix/MatrixOpenIdClient";
|
|
||||||
import Upstream from "../../db/models/Upstream";
|
|
||||||
import { ScalarClient } from "../../scalar/ScalarClient";
|
|
||||||
import User from "../../db/models/User";
|
|
||||||
import UserScalarToken from "../../db/models/UserScalarToken";
|
|
||||||
import { LogService } from "matrix-js-snippets";
|
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
import * as randomString from "random-string";
|
|
||||||
import { OpenId } from "../../models/OpenId";
|
import { OpenId } from "../../models/OpenId";
|
||||||
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
|
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
|
||||||
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
import { ScalarStore } from "../../db/ScalarStore";
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
interface RegisterRequest {
|
|
||||||
access_token: string;
|
|
||||||
token_type: string;
|
|
||||||
matrix_server_name: string;
|
|
||||||
expires_in: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API for the minimum Scalar API we need to implement to be compatible with clients. Used for registration
|
* API for the minimum Scalar API we need to implement to be compatible with clients. Used for registration
|
||||||
* and general account management.
|
* and general account management.
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/scalar")
|
@Path("/api/v1/scalar")
|
||||||
|
@AutoWired
|
||||||
export class ScalarService {
|
export class ScalarService {
|
||||||
|
|
||||||
/**
|
@Inject
|
||||||
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
|
private accountController: AccountController;
|
||||||
* @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 static async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> {
|
|
||||||
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
|
|
||||||
if (cachedUserId) return cachedUserId;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams);
|
|
||||||
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
|
|
||||||
return user.userId;
|
|
||||||
} catch (err) {
|
|
||||||
LogService.error("ScalarService", err);
|
|
||||||
throw new ApiError(401, "Invalid token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("register")
|
@Path("register")
|
||||||
public async register(request: RegisterRequest, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
|
public async register(request: OpenId, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
|
||||||
if (apiVersion !== "1.1") {
|
if (apiVersion !== "1.1") {
|
||||||
throw new ApiError(401, "Invalid API version.");
|
throw new ApiError(401, "Invalid API version.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const mxClient = new MatrixOpenIdClient(<OpenId>request);
|
const response = await this.accountController.registerAccount(request);
|
||||||
const mxUserId = await mxClient.getUserId();
|
return {scalar_token: response.token};
|
||||||
|
|
||||||
if (!mxUserId.endsWith(":" + request.matrix_server_name)) {
|
|
||||||
LogService.warn("ScalarService", `OpenID subject '${mxUserId}' does not belong to the homeserver '${request.matrix_server_name}'`);
|
|
||||||
throw new ApiError(401, "Invalid token");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findByPrimary(mxUserId);
|
|
||||||
if (!user) {
|
|
||||||
// There's a small chance we'll get a validation error because of:
|
|
||||||
// https://github.com/vector-im/riot-web/issues/5846
|
|
||||||
LogService.verbose("ScalarService", "User " + mxUserId + " never seen before - creating");
|
|
||||||
await User.create({userId: mxUserId});
|
|
||||||
}
|
|
||||||
|
|
||||||
const upstreams = await Upstream.findAll();
|
|
||||||
await Promise.all(upstreams.map(async upstream => {
|
|
||||||
if (!await ScalarStore.isUpstreamOnline(upstream)) {
|
|
||||||
LogService.warn("ScalarService", `Skipping registration for ${mxUserId} on upstream ${upstream.id} (${upstream.name}) because it is offline`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const tokens = await UserScalarToken.findAll({where: {userId: mxUserId, upstreamId: upstream.id}});
|
|
||||||
if (!tokens || tokens.length === 0) {
|
|
||||||
LogService.info("ScalarService", "Registering " + mxUserId + " for a token at upstream " + upstream.id + " (" + upstream.name + ")");
|
|
||||||
const client = new ScalarClient(upstream);
|
|
||||||
const response = await client.register(<OpenId>request);
|
|
||||||
return UserScalarToken.create({
|
|
||||||
userId: mxUserId,
|
|
||||||
scalarToken: response.scalar_token,
|
|
||||||
isDimensionToken: false,
|
|
||||||
upstreamId: upstream.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).filter(token => !!token));
|
|
||||||
|
|
||||||
const dimensionToken = randomString({length: 25});
|
|
||||||
const dimensionScalarToken = await UserScalarToken.create({
|
|
||||||
userId: mxUserId,
|
|
||||||
scalarToken: dimensionToken,
|
|
||||||
isDimensionToken: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
LogService.info("ScalarService", mxUserId + " has registered for a scalar token successfully");
|
|
||||||
return {scalar_token: dimensionScalarToken.scalarToken};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -108,7 +34,7 @@ export class ScalarService {
|
||||||
throw new ApiError(401, "Invalid API version.");
|
throw new ApiError(401, "Invalid API version.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = await ScalarService.getTokenOwner(scalarToken);
|
const userId = await this.accountController.getTokenOwner(scalarToken);
|
||||||
return {user_id: userId};
|
return {user_id: userId};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { LogService } from "matrix-js-snippets";
|
||||||
import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache";
|
import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache";
|
||||||
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
|
||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
import { ScalarService } from "./ScalarService";
|
|
||||||
import moment = require("moment");
|
import moment = require("moment");
|
||||||
|
import { AutoWired, Inject } from "typescript-ioc/es6";
|
||||||
|
import AccountController from "../controllers/AccountController";
|
||||||
|
|
||||||
interface UrlPreviewResponse {
|
interface UrlPreviewResponse {
|
||||||
cached_response: boolean;
|
cached_response: boolean;
|
||||||
|
@ -22,12 +23,16 @@ interface UrlPreviewResponse {
|
||||||
* API for the minimum Scalar API for widget functionality in clients.
|
* API for the minimum Scalar API for widget functionality in clients.
|
||||||
*/
|
*/
|
||||||
@Path("/api/v1/scalar/widgets")
|
@Path("/api/v1/scalar/widgets")
|
||||||
|
@AutoWired
|
||||||
export class ScalarWidgetService {
|
export class ScalarWidgetService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private accountController: AccountController;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("title_lookup")
|
@Path("title_lookup")
|
||||||
public async titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
public async titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
|
||||||
await ScalarService.getTokenOwner(scalarToken);
|
await this.accountController.getTokenOwner(scalarToken);
|
||||||
|
|
||||||
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
|
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
|
||||||
if (cachedResult) {
|
if (cachedResult) {
|
||||||
|
|
56
src/api/security/MSCSecurity.ts
Normal file
56
src/api/security/MSCSecurity.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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 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 {
|
||||||
|
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"});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = header.substring("Bearer ".length);
|
||||||
|
req.user = <IMSCUser>{
|
||||||
|
userId: await this.accountController.getTokenOwner(token),
|
||||||
|
token: token,
|
||||||
|
};
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(req.query);
|
||||||
|
} 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 {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -142,7 +142,11 @@ module.exports = function () {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8184',
|
target: 'http://localhost:8184',
|
||||||
secure: false
|
secure: false
|
||||||
}
|
},
|
||||||
|
'/_matrix': {
|
||||||
|
target: 'http://localhost:8184',
|
||||||
|
secure: false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user