Re-implement the Scalar API in typescript

This is part of a rewrite for Dimension to better support integrations. Only the bare minimum scalar APIs are implemented at this point - dimension is non-functional.
This commit is contained in:
Travis Ralston 2017-12-17 19:22:09 -07:00
parent 7a8b27fa22
commit 826364e803
31 changed files with 1225 additions and 493 deletions

2
.gitignore vendored
View File

@ -8,6 +8,8 @@ db/*.db
start.sh
config/integrations/*_development.yaml
config/integrations/*_production.yaml
build/
dimension.db
# Logs
logs

View File

@ -9,3 +9,4 @@ install:
- npm install
script:
- npm run build
- npm run lint:app

View File

@ -1,13 +0,0 @@
{
"defaultEnv": {
"ENV": "NODE_ENV"
},
"development": {
"driver": "sqlite",
"filename": "db/development.db"
},
"production": {
"driver": "sqlite",
"filename": "db/production.db"
}
}

View File

@ -3,6 +3,28 @@ web:
port: 8184
address: '0.0.0.0'
# Homeserver configuration (used to proxy some requests to the homeserver for processing)
homeserver:
name: "t2bot.io"
accessToken: "something"
# These users can modify the integrations this Dimension supports.
# To access the admin interface, open Dimension in Riot and click the settings icon.
admins:
- "@someone:domain.com"
# IPs and CIDR ranges listed here will be blocked from being widgets.
# Note: Widgets may still be embedded with restricted content, although not through Dimension directly.
widgetBlacklist:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
# Where the database for Dimension is
database:
file: "dimension.db"
# Settings for controlling how logging works
logging:
file: logs/dimension.log
@ -11,29 +33,4 @@ logging:
fileLevel: verbose
rotate:
size: 52428800 # bytes, default is 50mb
count: 5
# Demo bot configuration. Used purely to show how to configure a self-hosted bot in Dimension
demobot:
enabled: false
userId: "@dimension:t2bot.io"
homeserverUrl: "https://t2bot.io"
accessToken: "something"
# Upstream configuration. This should almost never change.
upstreams:
- name: vector
url: "https://scalar.vector.im/api"
# Homeserver configuration (used to proxy some requests to the homeserver for processing)
homeserver:
name: "t2bot.io"
accessToken: "something"
# IPs and CIDR ranges listed here will be blocked from being widgets.
# Note: Widgets may still be embedded with restricted content, although not through Dimension directly.
widgetBlacklist:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
count: 5

View File

View File

@ -1,34 +0,0 @@
'use strict';
var dbm;
var type;
var seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.createTable("tokens", {
id: {type: 'int', primaryKey: true, autoIncrement: true, notNull: true},
matrixUserId: {type: 'string', notNull: true},
matrixServerName: {type: 'string', notNull: true},
matrixAccessToken: {type: 'string', notNull: true},
scalarToken: {type: 'string', notNull: true},
expires: {type: 'timestamp', notNull: true}
});
};
exports.down = function (db) {
return db.dropTable("tokens");
};
exports._meta = {
"version": 1
};

View File

@ -1,27 +0,0 @@
'use strict';
var dbm;
var type;
var seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function (db) {
return db.addColumn('tokens', 'upstreamToken', {type: 'string', notNull: false}); // has to be nullable, despite our best intentions
};
exports.down = function (db) {
return db.removeColumn('tokens', 'upstreamToken');
};
exports._meta = {
"version": 1
};

866
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,46 +2,49 @@
"name": "matrix-dimension",
"version": "1.0.0",
"description": "An alternative integrations manager for Riot",
"main": "app.js",
"main": "build/app/index.js",
"license": "GPL-3.0",
"scripts": {
"dev": "webpack-dev-server --inline --progress --port 8080 --host 0.0.0.0",
"build": "rimraf web-dist && webpack --progress --profile --bail"
"start:dev": "webpack-dev-server --inline --progress --port 8080 --host 0.0.0.0",
"start:app": "npm run-script build && node build/app/index.js",
"build": "rimraf build && npm run-script build:web && npm run-script build:app",
"build:web": "webpack --progress --profile --bail",
"build:app": "tsc -p tsconfig-app.json",
"lint:app": "tslint --project ./tsconfig-app.json -t stylish"
},
"repository": {
"type": "git",
"url": "git+https://github.com/turt2live/matrix-dimension.git"
},
"greenkeeper": {
"ignore": [
"@types/node"
]
},
"author": "Travis Ralston",
"dependencies": {
"@types/body-parser": "^1.16.8",
"@types/node": "^8.5.1",
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"chalk": "^2.3.0",
"config": "^1.28.1",
"db-migrate": "^0.10.2",
"db-migrate-sqlite3": "^0.2.1",
"dns-then": "^0.1.0",
"embed-video": "^2.0.0",
"express": "^4.16.2",
"js-yaml": "^3.10.0",
"lodash": "^4.17.4",
"matrix-js-sdk": "^0.8.5",
"matrix-js-sdk": "^0.9.2",
"matrix-js-snippets": "^0.2.5",
"memory-cache": "^0.2.0",
"mime": "^2.0.3",
"moment": "^2.19.3",
"netmask": "^1.0.6",
"random-string": "^0.2.0",
"request": "^2.83.0",
"screenfull": "^3.3.2",
"require-dir-all": "^0.4.12",
"sequelize": "^4.27.0",
"sequelize-typescript": "^0.6.1",
"sqlite3": "^3.1.13",
"url": "^0.11.0",
"winston": "^2.4.0"
"typescript-rest": "^1.2.0",
"url": "^0.11.0"
},
"devDependencies": {
"embed-video": "^2.0.0",
"screenfull": "^3.3.2",
"@angular/animations": "^5.0.0",
"@angular/common": "^5.0.0",
"@angular/compiler": "^5.0.0",
@ -55,12 +58,11 @@
"@angularclass/hmr-loader": "^3.0.2",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.7",
"@types/jquery": "^3.2.16",
"@types/node": "^6.0.92",
"angular2-template-loader": "^0.6.2",
"angular2-toaster": "^4.0.0",
"angular2-ui-switch": "^1.2.0",
"awesome-typescript-loader": "^3.4.1",
"codelyzer": "^3.2.2",
"codelyzer": "^4.0.2",
"copy-webpack-plugin": "^4.2.3",
"core-js": "^2.5.2",
"css-loader": "^0.28.7",
@ -73,10 +75,10 @@
"jquery": "^3.2.1",
"json-loader": "^0.5.4",
"ng2-breadcrumbs": "^0.1.281",
"ngx-modialog": "^3.0.4",
"ngx-modialog": "^5.0.0",
"node-sass": "^4.7.2",
"postcss-cssnext": "^3.0.0",
"postcss-import": "^10.0.0",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.9",
"postcss-scss": "^1.0.0",
"raw-loader": "^0.5.1",
@ -86,12 +88,12 @@
"sass-loader": "^6.0.3",
"shelljs": "^0.7.8",
"spinkit": "^1.2.5",
"style-loader": "^0.18.2",
"style-loader": "^0.19.1",
"ts-helpers": "^1.1.2",
"tslint": "^5.8.0",
"tslint-loader": "^3.4.3",
"typescript": "^2.6.2",
"url-loader": "^0.5.8",
"url-loader": "^0.6.2",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.9.7",
"zone.js": "^0.8.18"

25
src-ts/MemoryCache.ts Normal file
View File

@ -0,0 +1,25 @@
import * as cache from "memory-cache";
export class MemoryCache {
private internalCache = new cache.Cache();
constructor() {
}
public put(key: string, value: any, timeoutMs?: number): void {
this.internalCache.put(key, value, timeoutMs);
}
public get(key: string): any {
return this.internalCache.get(key);
}
public del(key: string): void {
this.internalCache.del(key);
}
public clear(): void {
this.internalCache.clear();
}
}

14
src-ts/api/ApiError.ts Normal file
View File

@ -0,0 +1,14 @@
export class ApiError {
public statusCode: number;
public jsonResponse: any;
constructor(statusCode: number, json: any) {
// Because typescript is just plain dumb
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript
Error.apply(this, ["ApiError"]);
this.jsonResponse = json;
this.statusCode = statusCode;
}
}

76
src-ts/api/Webserver.ts Normal file
View File

@ -0,0 +1,76 @@
import * as express from "express";
import * as path from "path";
import * as bodyParser from "body-parser";
import * as URL from "url";
import { LogService } from "matrix-js-snippets";
import { Server } from "typescript-rest";
import * as _ from "lodash";
import config from "../config";
import { ApiError } from "./ApiError";
export default class Webserver {
private app: express.Application;
constructor() {
this.app = express();
this.configure();
this.loadRoutes();
}
private loadRoutes() {
const apis = ["scalar", "dimension"].map(a => path.join(__dirname, a, "*.js"));
const router = express.Router();
Server.loadServices(router, apis);
const routes = _.uniq(router.stack.map(r => r.route.path));
for (const route of routes) {
this.app.options(route, (_req, res) => res.sendStatus(200));
LogService.info("Webserver", "Registered route: " + route);
}
this.app.use(router);
// We register the default route last to make sure we don't override anything by accident.
// We'll pass off all other requests to the web app
this.app.get("*", (_req, res) => {
res.sendFile(path.join(__dirname, "..", "..", "web", "index.html"));
});
// Set up the error handler
this.app.use((err: any, _req, res, next) => {
if (err instanceof ApiError) {
// Don't do anything for 'connection reset'
if (res.headersSent) return next(err);
LogService.warn("Webserver", "Handling ApiError " + err.statusCode + " " + JSON.stringify(err.jsonResponse));
res.setHeader("Content-Type", "application/json");
res.status(err.statusCode);
res.json(err.jsonResponse);
} else next(err);
});
}
private configure() {
this.app.use(express.static(path.join(__dirname, "..", "..", "web")));
this.app.use(bodyParser.json());
this.app.use((req, _res, next) => {
const parsedUrl = URL.parse(req.url, true);
if (parsedUrl.query && parsedUrl.query["scalar_token"]) {
parsedUrl.query["scalar_token"] = "redacted";
parsedUrl.search = undefined; // to force URL.format to use `query`
}
LogService.verbose("Webserver", "Incoming request: " + req.method + " " + URL.format(parsedUrl));
next();
});
this.app.use((_req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
}
start() {
this.app.listen(config.web.port, config.web.address);
LogService.info("Webserver", "API and UI listening on " + config.web.address + ":" + config.web.port);
}
}

View File

@ -0,0 +1,106 @@
import { GET, Path, POST, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
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 * as randomString from "random-string";
import { OpenId } from "../../models/OpenId";
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
import { DimensionStore } from "../../db/DimensionStore";
import { MemoryCache } from "../../MemoryCache";
interface RegisterRequest {
access_token: string;
token_type: string;
matrix_server_name: string;
expires_in: number;
}
@Path("/api/v1/scalar")
export class ScalarService {
private static accountCache = new MemoryCache();
public static clearAccountCache(): void {
ScalarService.accountCache.clear();
}
public static getTokenOwner(scalarToken: string): Promise<string> {
const cachedUserId = ScalarService.accountCache.get(scalarToken);
if (cachedUserId) return Promise.resolve(cachedUserId);
return DimensionStore.getTokenOwner(scalarToken).then(user => {
if (!user) return Promise.reject("Invalid token");
ScalarService.accountCache.put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
return Promise.resolve(user.userId);
});
}
@POST
@Path("register")
public register(request: RegisterRequest): Promise<ScalarRegisterResponse> {
let userId = null;
const mxClient = new MatrixOpenIdClient(<OpenId>request);
return mxClient.getUserId().then(mxUserId => {
userId = mxUserId;
return User.findByPrimary(userId).then(user => {
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 " + userId + " never seen before - creating");
return User.create({userId: userId});
}
});
}).then(() => {
return Upstream.findAll();
}).then(upstreams => {
return Promise.all(upstreams.map(u => {
return UserScalarToken.findAll({where: {userId: userId, upstreamId: u.id}}).then(tokens => {
if (!tokens || tokens.length === 0) {
LogService.info("ScalarService", "Registering " + userId + " for token at upstream " + u.id + " (" + u.name + ")");
const client = new ScalarClient(u);
return client.register(<OpenId>request).then(registerResponse => {
return UserScalarToken.create({
userId: userId,
scalarToken: registerResponse.scalar_token,
isDimensionToken: false,
upstreamId: u.id,
});
});
}
});
}));
}).then(() => {
const dimensionToken = randomString({length: 25});
return UserScalarToken.create({
userId: userId,
scalarToken: dimensionToken,
isDimensionToken: true,
});
}).then(userToken => {
return {scalar_token: userToken.scalarToken};
}).catch(err => {
LogService.error("ScalarService", err);
throw new ApiError(401, {message: "Failed to authenticate user"});
});
}
@GET
@Path("account")
public getAccount(@QueryParam("scalar_token") scalarToken: string): Promise<ScalarAccountResponse> {
return ScalarService.getTokenOwner(scalarToken).then(userId => {
return {user_id: userId};
}, err => {
if (err !== "Invalid token") {
LogService.error("ScalarService", "Error getting account information for user");
LogService.error("ScalarService", err);
}
throw new ApiError(401, {message: "Invalid token"});
});
}
}

View File

@ -0,0 +1,81 @@
import { GET, Path, QueryParam } from "typescript-rest";
import * as Promise from "bluebird";
import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError";
import { MemoryCache } from "../../MemoryCache";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
import config from "../../config";
import { ScalarService } from "./ScalarService";
import moment = require("moment");
interface UrlPreviewResponse {
cached_response: boolean;
page_title_cache_item: {
expires: string; // "2017-12-18T04:20:04.001806738Z"
cached_response_err: string;
cached_title: string; // the actual thing riot uses
};
error: {
message: string;
};
}
@Path("/api/v1/scalar/widgets")
export class ScalarWidgetService {
private static urlCache = new MemoryCache();
private static getUrlTitle(url: string): Promise<UrlPreviewResponse> {
const cachedResult = ScalarWidgetService.urlCache.get(url);
if (cachedResult) {
cachedResult.cached_response = true;
return Promise.resolve(cachedResult);
}
const client = new MatrixLiteClient(config.homeserver.name, config.homeserver.accessToken);
return client.getUrlPreview(url).then(preview => {
const expirationTime = 60 * 80 * 1000; // 1 hour
const expirationAsString = moment().add(expirationTime, "milliseconds").toISOString();
const cachedItem = {
cached_response: false, // we're not cached yet
page_title_cache_item: {
expires: expirationAsString,
cached_response_err: null,
cached_title: preview["og:title"],
},
error: {message: null},
};
ScalarWidgetService.urlCache.put(url, cachedItem, expirationTime);
return cachedItem;
}).catch(err => {
LogService.error("ScalarWidgetService", "Error getting URL preview");
LogService.error("ScalarWidgetService", err);
return <UrlPreviewResponse>{
// All of this is to match scalar's response :/
cached_response: false,
page_title_cache_item: {
expires: null,
cached_response_err: "Failed to get URL preview",
cached_title: null
},
error: {
message: "Failed to get URL preview",
},
};
})
}
@GET
@Path("title_lookup")
public register(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
return ScalarService.getTokenOwner(scalarToken).then(_userId => {
return ScalarWidgetService.getUrlTitle(url);
}, err => {
if (err !== "Invalid token") {
LogService.error("ScalarWidgetService", "Error getting account information for user");
LogService.error("ScalarWidgetService", err);
}
throw new ApiError(401, {message: "Invalid token"});
})
}
}

21
src-ts/config.ts Normal file
View File

@ -0,0 +1,21 @@
import * as config from "config";
import { LogConfig } from "matrix-js-snippets";
export interface DimensionConfig {
web: {
port: number;
address: string;
};
homeserver: {
name: string;
accessToken: string;
};
widgetBlacklist: string[];
database: {
file: string;
};
admins: string[];
logging: LogConfig;
}
export default <DimensionConfig>config;

View File

@ -0,0 +1,75 @@
import { Model, Sequelize } from "sequelize-typescript";
import config from "../config";
import { LogService } from "matrix-js-snippets";
import User from "./models/User";
import UserScalarToken from "./models/UserScalarToken";
import Upstream from "./models/Upstream";
import * as Promise from "bluebird";
class _DimensionStore {
private sequelize: Sequelize;
constructor() {
this.sequelize = new Sequelize({
dialect: 'sqlite',
database: "dimension",
storage: config.database.file,
username: "",
password: "",
logging: i => LogService.verbose("DimensionStore [SQL]", i)
});
this.sequelize.addModels(<Array<typeof Model>>[
User,
UserScalarToken,
Upstream,
]);
}
public updateSchema(): Promise<any> {
LogService.info("DimensionStore", "Updating schema...");
return this.sequelize.sync();
}
public doesUserHaveTokensForAllUpstreams(userId: string): Promise<boolean> {
let upstreamTokenIds: number[] = [];
let hasDimensionToken = false;
return UserScalarToken.findAll({where: {userId: userId}}).then(results => {
upstreamTokenIds = results.filter(t => !t.isDimensionToken).map(t => t.upstreamId);
hasDimensionToken = results.filter(t => t.isDimensionToken).length >= 1;
return Upstream.findAll();
}).then(upstreams => {
if (!hasDimensionToken) {
LogService.warn("DimensionStore", "User " + userId + " is missing a Dimension scalar token");
return false;
}
for (const upstream of upstreams) {
if (upstreamTokenIds.indexOf(upstream.id) === -1) {
LogService.warn("DimensionStore", "User " + userId + " is missing a scalar token for upstream " + upstream.id + " (" + upstream.name + ")");
return false;
}
}
return true;
});
}
public getTokenOwner(scalarToken: string): Promise<User> {
let user: User = null;
return UserScalarToken.findAll({where: {isDimensionToken: true, scalarToken: scalarToken}, include: [User]}).then(tokens => {
if (!tokens || tokens.length === 0) {
return Promise.reject("Invalid token");
}
user = tokens[0].user;
return this.doesUserHaveTokensForAllUpstreams(user.userId);
}).then(hasUpstreams => {
if (!hasUpstreams) {
return Promise.reject("Invalid token"); // missing one or more upstreams == no validation
}
return Promise.resolve(user);
});
}
}
export const DimensionStore = new _DimensionStore();

View File

@ -0,0 +1,25 @@
import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-typescript";
@Table({
tableName: "dimension_upstreams",
underscoredAll: false,
timestamps: false,
})
export default class Upstream extends Model<Upstream> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
name: string;
@Column
type: string;
@Column
scalarUrl: string;
@Column
apiUrl: string;
}

14
src-ts/db/models/User.ts Normal file
View File

@ -0,0 +1,14 @@
import { Column, Model, PrimaryKey, Table } from "sequelize-typescript";
@Table({
tableName: "dimension_users",
underscoredAll: false,
timestamps: false,
})
export default class User extends Model<User> {
// This is really just a holding class to keep foreign keys alive
@PrimaryKey
@Column
userId: string;
}

View File

@ -0,0 +1,39 @@
import {
AllowNull, AutoIncrement, BelongsTo, Column, ForeignKey, Model, PrimaryKey,
Table
} from "sequelize-typescript";
import User from "./User";
import Upstream from "./Upstream";
@Table({
tableName: "dimension_scalar_tokens",
underscoredAll: false,
timestamps: false,
})
export default class UserScalarToken extends Model<UserScalarToken> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column
@ForeignKey(() => User)
userId: string;
@BelongsTo(() => User)
user: User;
@Column
scalarToken: string;
@Column
isDimensionToken: boolean;
@AllowNull
@Column
@ForeignKey(() => Upstream)
upstreamId?: number;
@BelongsTo(() => Upstream)
upstream: Upstream;
}

15
src-ts/index.ts Normal file
View File

@ -0,0 +1,15 @@
import { LogService } from "matrix-js-snippets";
import config from "./config";
import { DimensionStore } from "./db/DimensionStore";
import Webserver from "./api/Webserver";
LogService.configure(config.logging);
LogService.info("index", "Starting voyager...");
const webserver = new Webserver();
DimensionStore.updateSchema()
.then(() => webserver.start())
.then(() => {
LogService.info("index", "Dimension is ready!");
});

View File

@ -0,0 +1,24 @@
import * as Promise from "bluebird";
import { doFederatedApiCall } from "./helpers";
export interface MatrixUrlPreview {
// This is really the only parameter we care about
"og:title"?: string;
}
export class MatrixLiteClient {
constructor(private homeserverName: string, private accessToken: string) {
}
public getUrlPreview(url: string): Promise<MatrixUrlPreview> {
return doFederatedApiCall(
"GET",
this.homeserverName,
"/_matrix/media/r0/preview_url",
{access_token: this.accessToken, url: url}
).then(response => {
return response;
});
}
}

View File

@ -0,0 +1,20 @@
import * as Promise from "bluebird";
import { doFederatedApiCall } from "./helpers";
import { OpenId } from "../models/OpenId";
export class MatrixOpenIdClient {
constructor(private openId: OpenId) {
}
public getUserId(): Promise<string> {
return doFederatedApiCall(
"GET",
this.openId.matrix_server_name,
"/_matrix/federation/v1/openid/userinfo",
{access_token: this.openId.access_token}
).then(response => {
return response['sub'];
});
}
}

61
src-ts/matrix/helpers.ts Normal file
View File

@ -0,0 +1,61 @@
import * as dns from "dns-then";
import * as Promise from "bluebird";
import { LogService } from "matrix-js-snippets";
import { MemoryCache } from "../MemoryCache";
import * as request from "request";
const federationUrlCache = new MemoryCache();
export function getFederationUrl(serverName: string): Promise<string> {
const cachedUrl = federationUrlCache.get(serverName);
if (cachedUrl) {
LogService.verbose("matrix", "Cached federation URL for " + serverName + " is " + cachedUrl);
return Promise.resolve(cachedUrl);
}
let serverUrl = null;
let expirationMs = 4 * 60 * 60 * 1000; // default is 4 hours
const dnsPromise = dns.resolveSrv("_matrix._tcp." + serverName);
return Promise.resolve(dnsPromise).then(records => {
if (records && records.length > 0) {
serverUrl = "https://" + records[0].name + ":" + records[0].port;
expirationMs = records[0].ttl * 1000;
}
}, _err => {
// Not having the SRV record isn't bad, it just means that the server operator decided to not use SRV records.
// When there's no SRV record we default to port 8448 (as per the federation rules) in the lower .then()
// People tend to think that the lack of an SRV record is bad, but in reality it's only a problem if one was set and
// it's not being found. Most people don't set up the SRV record, but some do.
LogService.warn("matrix", "Could not find _matrix._tcp." + serverName + " DNS record. This is normal for most servers.");
}).then(() => {
if (!serverUrl) serverUrl = "https://" + serverName + ":8448";
LogService.verbose("matrix", "Federation URL for " + serverName + " is " + serverUrl + " - caching for " + expirationMs + " ms");
federationUrlCache.put(serverName, serverUrl, expirationMs);
return serverUrl;
});
}
export function doFederatedApiCall(method: string, serverName: string, endpoint: string, query?: object, body?:object):Promise<any> {
return getFederationUrl(serverName).then(federationUrl => {
return new Promise((resolve, reject) => {
request({
method: method,
url: federationUrl + endpoint,
qs: query,
json: body,
rejectUnauthorized: false, // allow self signed certs (for federation)
}, (err, res, _body) => {
if (err) {
LogService.error("matrix", "Error calling " + endpoint);
LogService.error("matrix", err);
reject(err);
} else if (res.statusCode !== 200) {
LogService.error("matrix", "Got status code " + res.statusCode + " while calling " + endpoint);
reject(new Error("Error in request: invalid status code"));
} else {
resolve(res.body);
}
});
});
});
}

6
src-ts/models/OpenId.ts Normal file
View File

@ -0,0 +1,6 @@
export interface OpenId {
access_token: string;
matrix_server_name: string;
expires_in: number;
token_type: 'Bearer';
}

View File

@ -0,0 +1,8 @@
export interface ScalarRegisterResponse {
scalar_token: string;
}
export interface ScalarAccountResponse {
user_id: string;
//credit: number; // present on scalar-web
}

View File

@ -0,0 +1,32 @@
import { OpenId } from "../models/OpenId";
import { ScalarRegisterResponse } from "../models/ScalarResponses";
import * as Promise from "bluebird";
import * as request from "request";
import { LogService } from "matrix-js-snippets";
import Upstream from "../db/models/Upstream";
export class ScalarClient {
constructor(private upstream: Upstream) {
}
public register(openId: OpenId): Promise<ScalarRegisterResponse> {
return new Promise((resolve, reject) => {
request({
method: "POST",
url: this.upstream.scalarUrl + "/register",
json: openId,
}, (err, res, _body) => {
if (err) {
LogService.error("ScalarClient", "Error registering for token");
LogService.error("ScalarClient", err);
reject(err);
} else if (res.statusCode !== 200) {
LogService.error("ScalarClient", "Got status code " + res.statusCode + " while registering for token");
reject(new Error("Could not get token"));
} else {
resolve(res.body);
}
});
});
}
}

5
src-ts/temp_todo.txt Normal file
View File

@ -0,0 +1,5 @@
* dimension apis
* integration management
* admin ui
* delete old src
* import from old db/config (script)

18
tsconfig-app.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2015",
"noImplicitAny": false,
"sourceMap": true,
"outDir": "./build/app",
"types": [
"node"
]
},
"include": [
"./src-ts/**/*"
]
}

View File

@ -9,7 +9,7 @@
"check-space"
],
"curly": false,
"eofline": true,
"eofline": false,
"forin": false,
"indent": [
true,
@ -107,9 +107,6 @@
],
"no-attribute-parameter-decorator": true,
"no-forward-ref": true,
"import-destructuring-spacing": true,
"no-access-missing-member": true,
"templates-use-public": true,
"invoke-injectable": true
"import-destructuring-spacing": true
}
}

View File

@ -11,7 +11,7 @@
border: 1px solid #eee;
border-radius: 5px;
margin: 7px;
padding: 5px;
padding: 6px;
width: calc(325px - 14px);
position: relative;
cursor: pointer;
@ -21,7 +21,7 @@
display: none;
position: absolute;
top: calc(50% - 12px); // icon happens to be 24px tall
right: 0;
right: 1px;
color: #bbb;
}

View File

@ -6,7 +6,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var isProd = process.env.npm_lifecycle_event == 'build';
var isProd = process.env.npm_lifecycle_event === 'build';
module.exports = function () {
var config = {};
@ -21,7 +21,7 @@ module.exports = function () {
};
config.output = {
path: root('web-dist'),
path: path.join(root('build'), 'web'),
publicPath: isProd ? '/' : '/', //http://0.0.0.0:8080',
filename: isProd ? 'js/[name].[hash].js' : 'js/[name].js',
chunkFilename: isProd ? '[id].[hash].chunk.js' : '[id].chunk.js'