Merge pull request #429 from TheTimeWalker/update-code

Fully update codebase to Angular 12, Typescript 4 and more
This commit is contained in:
Travis Ralston 2021-09-06 12:35:39 -06:00 committed by GitHub
commit 147a536402
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
243 changed files with 13589 additions and 9132 deletions

17
.browserslistrc Normal file
View File

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
build
# don't lint nyc coverage output
coverage

120
.eslintrc.json Normal file
View File

@ -0,0 +1,120 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/indent": "error",
"@typescript-eslint/member-delimiter-style": [
"off",
{
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/member-ordering": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/quotes": "off",
"@typescript-eslint/semi": [
"off",
null
],
"@typescript-eslint/type-annotation-spacing": "error",
"brace-style": [
"error",
"1tbs"
],
"curly": "off",
"eol-last": "off",
"eqeqeq": [
"error",
"always"
],
"guard-for-in": "off",
"id-blacklist": "off",
"id-match": "off",
"max-len": "off",
"no-bitwise": "off",
"no-caller": "error",
"no-console": "off",
"no-debugger": "error",
"no-empty": "off",
"no-eval": "error",
"no-fallthrough": "error",
"no-new-wrappers": "error",
"no-redeclare": "error",
"no-shadow": [
"error",
{
"hoist": "all"
}
],
"no-trailing-spaces": "error",
"no-underscore-dangle": "off",
"no-unused-labels": "error",
"no-var": "error",
"radix": "error"
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

2
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -1,4 +1,4 @@
FROM node:12.16.1-alpine AS builder
FROM node:14.17.5-alpine AS builder
LABEL maintainer="Andreas Peters <support@aventer.biz>"
#Upstream URL: https://git.aventer.biz/AVENTER/docker-matrix-dimension
@ -17,7 +17,7 @@ RUN npm clean-install && \
node /home/node/matrix-dimension/scripts/convert-newlines.js /home/node/matrix-dimension/docker-entrypoint.sh && \
NODE_ENV=production npm run-script build
FROM node:12.16.1-alpine
FROM node:14.17.5-alpine
WORKDIR /home/node/matrix-dimension

119
angular.json Normal file
View File

@ -0,0 +1,119 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"dimension": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "web",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "build/web",
"index": "web/index.html",
"main": "web/main.ts",
"polyfills": "web/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"web/assets"
],
"styles": [
"web/style/app.scss"
],
"scripts": [],
"progress": true
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "4mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "32kb"
}
],
"fileReplacements": [
{
"replace": "web/environments/environment.ts",
"with": "web/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "dimension:build:production"
},
"development": {
"browserTarget": "dimension:build:development"
}
},
"defaultConfiguration": "development",
"options": {
"proxyConfig": "proxy.conf.json"
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "dimension:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "web/test.ts",
"polyfills": "web/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"web/favicon.ico",
"web/assets"
],
"styles": [],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"defaultProject": "dimension",
"cli": {
"defaultCollection": "@angular-eslint/schematics"
}
}

7
config/bot.json Normal file
View File

@ -0,0 +1,7 @@
{
"syncToken": "s29_4025_0_1_40_1_26_19_1",
"filter": null,
"appserviceUsers": {},
"appserviceTransactions": {},
"kvStore": {}
}

6
config/bot.json.bak Normal file
View File

@ -0,0 +1,6 @@
{
"syncToken": "s3510138_39116385_25484_5602626_218935_269_119102_8310593_32",
"filter": null,
"appserviceUsers": {},
"appserviceTransactions": {}
}

BIN
config/dimension.db.bak Normal file

Binary file not shown.

BIN
config/dimension.db.bak2 Normal file

Binary file not shown.

View File

@ -1,8 +1,10 @@
#!/bin/sh
echo "Starting matrix-dimension"
if [ -f "/data/config.yaml" ]; then
cp /data/config.yaml /home/node/matrix-dimension/config/production.yaml
NODE_ENV=production node build/app/index.js
NODE_ENV=production exec node build/app/index.js
else
cp /home/node/matrix-dimension/config/default.yaml /data/config.yaml
echo "A default config file has been placed in the /data/ volume please review and make any required changes and start the container again"

45
karma.conf.js Normal file
View File

@ -0,0 +1,45 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/angular-starter'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

17430
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,15 +5,16 @@
"main": "build/app/index.js",
"license": "GPL-3.0",
"scripts": {
"start:web": "webpack-dev-server --inline --mode=development --progress --port 8082 --host 0.0.0.0",
"ng": "ng",
"start:web": "ng serve --configuration development --port 8082",
"start:app": "npm run-script build && node build/app/index.js",
"start:apponly": "npm run-script build:app && node build/app/index.js",
"start:apponly": "ts-node-dev --respawn --transpile-only --project tsconfig.backend.json ./src/index.ts",
"build": "npm run-script build:web && npm run-script build:app",
"build:web": "rimraf build/web && webpack --mode production --progress --profile --bail",
"build:app": "rimraf build/app && tsc -p tsconfig-app.json",
"lint": "npm run-script lint:app && npm run-script lint:web",
"lint:app": "tslint --project ./tsconfig-app.json -t stylish",
"lint:web": "tslint --project ./tsconfig.json -t stylish",
"build:web": "rimraf build/web && ng build --configuration production",
"build:app": "rimraf build/app && tsc -p tsconfig.backend.json",
"lint": "eslint .",
"lint:app": "eslint src",
"lint:web": "eslint web",
"i18n": "npm run-script i18n:init && npm run-script i18n:extract",
"i18n:init": "ngx-translate-extract --input ./web --output ./web/public/assets/i18n/template.json --key-as-default-value --replace --format json",
"i18n:extract": "ngx-translate-extract --input ./web --output ./web/public/assets/i18n/en.json --clean --format json"
@ -24,112 +25,121 @@
},
"author": "Travis Ralston",
"dependencies": {
"@ngx-translate/core": "^12.1.2",
"@angular-devkit/build-angular": "^12.2.1",
"@angular/animations": "^12.2.2",
"@angular/common": "^12.2.2",
"@angular/compiler": "^12.2.2",
"@angular/core": "^12.2.2",
"@angular/forms": "^12.2.2",
"@angular/localize": "^12.2.2",
"@angular/platform-browser": "^12.2.2",
"@angular/platform-browser-dynamic": "^12.2.2",
"@angular/router": "^12.2.2",
"@angularclass/hmr": "^3.0.0",
"@angularclass/hmr-loader": "^3.0.4",
"@fortawesome/angular-fontawesome": "^0.9.0",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@types/bluebird": "^3.5.27",
"@types/body-parser": "^1.17.0",
"@types/node": "^12.0.10",
"@types/validator": "^10.11.1",
"@popperjs/core": "^2.9.3",
"@webcomponents/custom-elements": "^1.5.0",
"body-parser": "^1.19.0",
"config": "^3.1.0",
"dns-then": "^0.1.0",
"config": "^3.3.6",
"express": "^4.17.1",
"git-rev-sync": "^1.12.0",
"isipaddress": "0.0.2",
"js-yaml": "^3.13.1",
"lodash": "^4.17.19",
"matrix-bot-sdk": "^0.3.8",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"matrix-bot-sdk": "^0.5.19",
"matrix-js-snippets": "^0.2.8",
"memory-cache": "^0.2.0",
"mime": "^2.4.2",
"moment": "^2.24.0",
"netmask": "^1.0.6",
"pg": "^8.5.1",
"mime": "^2.5.2",
"moment": "^2.29.1",
"netmask": "^2.0.2",
"ngx-ui-switch": "^12.0.1",
"pg": "^8.7.1",
"pg-hstore": "^2.3.4",
"postcss": "^8.3.6",
"random-string": "^0.2.0",
"request": "^2.88.0",
"request-promise": "^4.2.4",
"reflect-metadata": "^0.1.13",
"request": "^2.88.2",
"request-promise": "^4.2.6",
"require-dir-all": "^0.4.15",
"semver": "^6.0.0",
"sequelize": "^5.18.4",
"sequelize-typescript": "^1.0.0",
"sharp": "^0.25.3",
"rxjs": "^6.6.7",
"rxjs-compat": "^6.6.7",
"semver": "^7.3.5",
"sequelize": "6.6.2",
"sequelize-typescript": "^2.1.0",
"sharp": "^0.29.0",
"split-host": "^0.1.1",
"spotify-uri": "^1.0.0",
"sqlite3": "^4.2.0",
"telegraf": "^3.30.1",
"typescript": "^3.5.2",
"typescript-ioc": "^1.2.5",
"typescript-rest": "^2.2.0",
"umzug": "^2.2.0",
"spotify-uri": "^2.2.0",
"sqlite3": "^5.0.2",
"telegraf": "^4.4.1",
"typescript-rest": "^2.2.6",
"umzug": "^3.0.0-beta.16",
"url": "^0.11.0",
"xml2js": "^0.4.23"
"xml2js": "^0.4.23",
"xng-breadcrumb": "^6.7.0",
"zone.js": "^0.11.4"
},
"devDependencies": {
"@angular/animations": "^8.0.3",
"@angular/common": "^8.0.3",
"@angular/compiler": "^8.0.3",
"@angular/core": "^8.0.3",
"@angular/forms": "^8.0.3",
"@angular/platform-browser": "^8.0.3",
"@angular/platform-browser-dynamic": "^8.0.3",
"@angular/router": "^8.0.3",
"@angularclass/hmr": "^2.1.3",
"@angularclass/hmr-loader": "^3.0.4",
"@babel/core": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"@biesbjerg/ngx-translate-extract": "^7.0.3",
"@ckeditor/ckeditor5-angular": "^1.1.0",
"@ckeditor/ckeditor5-build-classic": "^12.2.0",
"@angular/cli": "^12.2.1",
"@angular/compiler-cli": "^12.2.2",
"@angular-eslint/builder": "12.3.1",
"@angular-eslint/eslint-plugin": "12.3.1",
"@angular-eslint/eslint-plugin-template": "12.3.1",
"@angular-eslint/schematics": "12.3.1",
"@angular-eslint/template-parser": "12.3.1",
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@biesbjerg/ngx-translate-extract": "^7.0.4",
"@ckeditor/ckeditor5-angular": "^2.0.2",
"@ckeditor/ckeditor5-build-classic": "^29.1.0",
"@fortawesome/fontawesome": "^1.1.8",
"@fortawesome/fontawesome-free-brands": "^5.0.13",
"@fortawesome/fontawesome-free-regular": "^5.0.13",
"@fortawesome/fontawesome-free-solid": "^5.0.13",
"@ng-bootstrap/ng-bootstrap": "^4.2.1",
"@types/jquery": "^3.3.30",
"@ng-bootstrap/ng-bootstrap": "^11.0.0-beta.2",
"@types/bluebird": "^3.5.36",
"@types/body-parser": "^1.19.1",
"@types/config": "0.0.39",
"@types/jquery": "^3.5.6",
"@types/node": "^14.17.4",
"@types/validator": "^13.6.3",
"@typescript-eslint/eslint-plugin": "4.28.2",
"@typescript-eslint/parser": "4.28.2",
"angular2-template-loader": "^0.6.2",
"angular2-toaster": "^7.0.0",
"angular2-ui-switch": "^1.2.0",
"awesome-typescript-loader": "^5.2.1",
"bootstrap": "^4.3.1",
"codelyzer": "^5.1.0",
"copy-webpack-plugin": "^5.1.2",
"core-js": "^3.1.4",
"css-loader": "^3.0.0",
"cssnano": "^4.1.10",
"angular2-toaster": "^11.0.1",
"bootstrap": "^5.1.0",
"css-loader": "^6.2.0",
"cssnano": "^5.0.8",
"dom-to-image": "^2.6.0",
"embed-video": "^2.0.4",
"file-loader": "^4.0.0",
"eslint": "^7.26.0",
"eslint-webpack-plugin": "^3.0.1",
"goby": "^1.1.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"iso-639-1": "^2.0.5",
"jquery": "^3.5.0",
"html-loader": "^2.1.2",
"html-webpack-plugin": "^5.3.2",
"iso-639-1": "^2.1.9",
"jquery": "^3.6.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^0.7.0",
"ng2-breadcrumbs": "^0.1.281",
"ngx-modialog": "^5.0.1",
"node-sass": "^4.14.1",
"mini-css-extract-plugin": "^2.2.0",
"postcss-cssnext": "^3.1.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-scss": "^2.0.0",
"raw-loader": "1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^2.6.3",
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
"sass-loader": "^7.1.0",
"screenfull": "^4.2.0",
"shelljs": "^0.8.3",
"spinkit": "^1.2.5",
"style-loader": "^0.23.1",
"ts-helpers": "^1.1.2",
"tslint": "^5.18.0",
"tslint-loader": "^3.5.4",
"url-loader": "^2.0.1",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.11.1",
"zone.js": "^0.9.1"
"postcss-import": "^14.0.2",
"postcss-loader": "^6.1.1",
"postcss-scss": "^4.0.0",
"rimraf": "^3.0.2",
"sass-loader": "^12.1.0",
"screenfull": "^5.1.0",
"spinkit": "^2.0.1",
"style-loader": "^3.2.1",
"ts-loader": "^9.2.5",
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8",
"typescript": "^4.3.5",
"webpack": "^5.50.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.0.0"
}
}
}

14
proxy.conf.json Normal file
View File

@ -0,0 +1,14 @@
{
"/api": {
"target": "http://localhost:8184",
"secure": false
},
"/_matrix": {
"target": "http://localhost:8184",
"secure": false
},
"/.well-known": {
"target": "http://localhost:8184",
"secure": false
}
}

View File

@ -26,9 +26,8 @@ export default class Webserver {
private loadRoutes() {
// TODO: Rename services to controllers, and controllers to services. They're backwards.
const apis = ["scalar", "dimension", "admin", "matrix"].map(a => path.join(__dirname, a, "*.js"));
const apis = ["scalar", "dimension", "admin", "matrix"].map(a => path.join(__dirname, a, "*"));
const router = express.Router();
Server.useIoC();
Server.registerAuthenticator(new MatrixSecurity());
apis.forEach(a => Server.loadServices(router, [a]));
const routes = _.uniq(router.stack.map(r => r.route.path));

View File

@ -89,7 +89,7 @@ export class AdminStickerService {
authorName: "Telegram",
authorReference: request.packUrl,
license: "Telegram",
licensePath: "/licenses/telegram-imported.txt",
licensePath: "/assets/licenses/telegram-imported.txt",
});
const stickers = [];

View File

@ -1,6 +1,5 @@
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 {
@ -13,44 +12,40 @@ interface CreatePolicyObject {
* 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();
return new 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);
return new 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);
return new 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);
return new 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);
return new TermsController().updatePolicy(request.name, shortcode, version, request.text, request.url);
}
}

View File

@ -8,7 +8,6 @@ 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 { ILoggedInUser } from "../security/MatrixSecurity";
@ -23,7 +22,6 @@ export interface IAccountInfoResponse {
/**
* API controller for account management
*/
@AutoWired
export default class AccountController {
constructor() {
}

View File

@ -1,4 +1,3 @@
import { AutoWired } from "typescript-ioc/es6";
import { ILoggedInUser } from "../security/MatrixSecurity";
import TermsRecord from "../../db/models/TermsRecord";
import TermsTextRecord from "../../db/models/TermsTextRecord";
@ -50,11 +49,7 @@ 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);

View File

@ -4,7 +4,6 @@ import { LogService } from "matrix-js-snippets";
import { URL } from "url";
import { BigBlueButtonGetJoinUrlRequest } from "../../models/Widget";
import { BigBlueButtonJoinResponse, BigBlueButtonCreateAndJoinMeetingResponse, BigBlueButtonWidgetResponse } from "../../models/WidgetResponses";
import { AutoWired } from "typescript-ioc/es6";
import { ApiError } from "../ApiError";
import { sha256 } from "../../utils/hashing";
import config from "../../config";
@ -16,7 +15,6 @@ import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
* API for the BigBlueButton widget.
*/
@Path("/api/v1/dimension/bigbluebutton")
@AutoWired
export class DimensionBigBlueButtonService {
/**
@ -113,7 +111,7 @@ export class DimensionBigBlueButtonService {
@Path("join")
public async join(
@QueryParam("greenlightUrl") greenlightURL: string,
@QueryParam("fullName") fullName: string,
@QueryParam("fullName") fullName: string,
): Promise<BigBlueButtonJoinResponse|ApiError> {
// Parse the greenlight url and retrieve the path
const greenlightMeetingID = new URL(greenlightURL).pathname;
@ -141,7 +139,7 @@ export class DimensionBigBlueButtonService {
// than following it ourselves
// Add authenticity token and full name to the query parameters
let queryParams = {authenticity_token: authenticityToken};
const queryParams = {authenticity_token: authenticityToken};
queryParams[`${greenlightMeetingID}[join_name]`] = fullName;
// Request the updated URL
@ -241,7 +239,7 @@ export class DimensionBigBlueButtonService {
}
},
"layout": {
"container": "top",
"container": "top",
"index": 0,
"width": 65,
"height": 50,
@ -261,7 +259,7 @@ export class DimensionBigBlueButtonService {
getJoinUrlRequest: BigBlueButtonGetJoinUrlRequest,
): Promise<BigBlueButtonCreateAndJoinMeetingResponse|ApiError> {
// Check if the meeting exists and is running. If not, return an error for each case
let getMeetingInfoParameters = {
const getMeetingInfoParameters = {
meetingID: getJoinUrlRequest.meetingId,
}
@ -292,7 +290,7 @@ export class DimensionBigBlueButtonService {
fullName = getJoinUrlRequest.userId;
}
let joinQueryParameters = {
const joinQueryParameters = {
meetingID: getJoinUrlRequest.meetingId,
password: getJoinUrlRequest.meetingPassword,
fullName: fullName,

View File

@ -29,7 +29,7 @@ export class DimensionWebhooksService {
@POST
@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<void> {
const webhook = await Webhook.findByPk(webhookId).catch(() => null);
if (!webhook) throw new ApiError(404, "Webhook not found");
if (!webhook.targetUrl) throw new ApiError(400, "Webhook not configured");

View File

@ -1,8 +1,8 @@
import { GET, Path, PathParam, QueryParam } from "typescript-rest";
import { LogService } from "matrix-js-snippets";
import * as url from "url";
import { promises as dnsPromises } from 'dns';
import { ApiError } from "../ApiError";
import * as dns from "dns-then";
import config from "../../config";
import { Netmask } from "netmask";
import * as request from "request";
@ -59,7 +59,7 @@ export class DimensionWidgetService {
const hostname = parsed.hostname.split(":")[0];
let addresses = [];
try {
addresses = await dns.resolve(hostname);
addresses = await dnsPromises.resolve(hostname);
} catch (err) {
LogService.error("DimensionWidgetService", err);
}
@ -76,7 +76,7 @@ export class DimensionWidgetService {
}
// Now we need to verify we can actually make the request
await new Promise((resolve, reject) => {
await new Promise<ApiError | void>((resolve, reject) => {
request(checkUrl, (err, response) => {
if (err) {
LogService.error("DimensionWidgetService", err);

View File

@ -1,7 +1,6 @@
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 { ILoggedInUser, ROLE_USER } from "../security/MatrixSecurity";
import { ScalarClient } from "../../scalar/ScalarClient";
@ -9,19 +8,15 @@ import { ScalarClient } from "../../scalar/ScalarClient";
* API for account management
*/
@Path("/_matrix/integrations/v1/account")
@AutoWired
export class MatrixAccountService {
@Inject
private accountController: AccountController;
@Context
private context: ServiceContext;
@POST
@Path("register")
public async register(request: OpenId): Promise<IAccountRegisteredResponse> {
return this.accountController.registerAccount(request, ScalarClient.KIND_MATRIX_V1);
return new AccountController().registerAccount(request, ScalarClient.KIND_MATRIX_V1);
}
@GET
@ -36,7 +31,7 @@ export class MatrixAccountService {
@Path("logout")
@Security(ROLE_USER)
public async logout(): Promise<any> {
await this.accountController.logout(this.context.request.user);
await new AccountController().logout(this.context.request.user);
return {};
}
}

View File

@ -1,5 +1,4 @@
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";
@ -11,26 +10,22 @@ export interface SignTermsRequest {
* 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();
return new 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);
await new TermsController().signTermsMatching(this.context.request.user, request.user_accepts);
return {};
}
}

View File

@ -1,5 +1,4 @@
import { GET, Path } from "typescript-rest";
import { AutoWired } from "typescript-ioc/es6";
import { URL } from "url";
import config from "../../config";
@ -16,7 +15,6 @@ interface WellknownResponse {
* Serving of the .well-known file
*/
@Path("/.well-known/matrix")
@AutoWired
export class MatrixWellknownService {
@GET

View File

@ -2,7 +2,6 @@ import { Context, GET, Path, POST, QueryParam, Security, ServiceContext } from "
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";
@ -14,15 +13,8 @@ import { ScalarClient } from "../../scalar/ScalarClient";
* and general account management.
*/
@Path("/api/v1/scalar")
@AutoWired
export class ScalarService {
@Inject
private accountController: AccountController;
@Inject
private termsController: TermsController;
@Context
private context: ServiceContext;
@ -33,7 +25,7 @@ export class ScalarService {
throw new ApiError(401, "Invalid API version.");
}
const response = await this.accountController.registerAccount(request, ScalarClient.KIND_LEGACY);
const response = await new AccountController().registerAccount(request, ScalarClient.KIND_LEGACY);
return {scalar_token: response.token};
}
@ -51,14 +43,14 @@ export class ScalarService {
@GET
@Path("terms")
public async getTerms(): Promise<ITermsResponse> {
return this.termsController.getAvailableTerms();
return new 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);
await new TermsController().signTermsMatching(this.context.request.user, request.user_accepts);
return {};
}

View File

@ -4,7 +4,7 @@ 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 moment from 'moment';
interface UrlPreviewResponse {
cached_response: boolean;

View File

@ -46,4 +46,5 @@ export interface DimensionConfig {
logging: LogConfig;
}
export default <DimensionConfig>config;
//TODO: We should better use the .get function from node config
export default config as unknown as DimensionConfig;

View File

@ -6,7 +6,7 @@ import UserScalarToken from "./models/UserScalarToken";
import Upstream from "./models/Upstream";
import WidgetRecord from "./models/WidgetRecord";
import * as path from "path";
import * as Umzug from "umzug";
import { SequelizeStorage, Umzug } from "umzug";
import AppService from "./models/AppService";
import AppServiceUser from "./models/AppServiceUser";
import NebConfiguration from "./models/NebConfiguration";
@ -79,15 +79,20 @@ class _DimensionStore {
}
public updateSchema(): Promise<any> {
LogService.info("DimensionStore", "Updating schema...");
LogService.info("DimensionStore", "Updating schema...",);
const migrator = new Umzug({
storage: "sequelize",
storageOptions: {sequelize: this.sequelize},
migrations: {
params: [this.sequelize.getQueryInterface()],
path: path.join(__dirname, "migrations"),
}
glob: path.join(__dirname, "migrations/*.{js,ts}"),
resolve: ({name, path, context}) => {
// Adjust the migration from the new signature to the v2 signature, making easier to upgrade to v3
const migration = require(path)
return { name, up: async () => migration.default.up(context), down: async () => migration.default.down(context) }
}
},
context: this.sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize: this.sequelize }),
logger: console
});
return migrator.up();

View File

@ -28,69 +28,69 @@ export class NebStore {
// TODO: Support Circle CI
// "circleci": {
// name: "Circle CI",
// avatarUrl: "/img/avatars/circleci.png",
// avatarUrl: "/assets/img/avatars/circleci.png",
// description: "Announces build results from Circle CI to the room.",
// simple: false,
// },
"echo": {
name: "Echo",
avatarUrl: "/img/avatars/echo.png", // TODO: Make this image
avatarUrl: "/assets/img/avatars/echo.png", // TODO: Make this image
description: "Repeats text given to it from !echo",
simple: true,
},
"giphy": {
name: "Giphy",
avatarUrl: "/img/avatars/giphy.png",
avatarUrl: "/assets/img/avatars/giphy.png",
description: "Posts a GIF from Giphy using !giphy <query>",
simple: true,
},
"guggy": {
name: "Guggy",
avatarUrl: "/img/avatars/guggy.png",
avatarUrl: "/assets/img/avatars/guggy.png",
description: "Send a reaction GIF using !guggy <query>",
simple: true,
},
// TODO: Support Github
// "github": {
// name: "Github",
// avatarUrl: "/img/avatars/github.png",
// avatarUrl: "/assets/img/avatars/github.png",
// description: "Github issue management and announcements for a repository",
// simple: false,
// },
"google": {
name: "Google",
avatarUrl: "/img/avatars/google.png",
avatarUrl: "/assets/img/avatars/google.png",
description: "Searches Google Images using !google image <query>",
simple: true,
},
"imgur": {
name: "Imgur",
avatarUrl: "/img/avatars/imgur.png",
avatarUrl: "/assets/img/avatars/imgur.png",
description: "Searches and posts images from Imgur using !imgur <query>",
simple: true,
},
// TODO: Support JIRA
// "jira": {
// name: "Jira",
// avatarUrl: "/img/avatars/jira.png",
// avatarUrl: "/assets/img/avatars/jira.png",
// description: "Jira issue management and announcements for a project",
// simple: false,
// },
"rss": {
name: "RSS",
avatarUrl: "/img/avatars/rssbot.png",
avatarUrl: "/assets/img/avatars/rssbot.png",
description: "Announces changes to RSS feeds in the room",
simple: false,
},
"travisci": {
name: "Travis CI",
avatarUrl: "/img/avatars/travisci.png",
avatarUrl: "/assets/img/avatars/travisci.png",
description: "Announces build results from Travis CI to the room",
simple: false,
},
"wikipedia": {
name: "Wikipedia",
avatarUrl: "/img/avatars/wikipedia.png",
avatarUrl: "/assets/img/avatars/wikipedia.png",
description: "Searches wikipedia using !wikipedia <query>",
simple: true,
},

View File

@ -0,0 +1,30 @@
import { QueryInterface } from "sequelize";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() =>
queryInterface.sequelize.query(
"UPDATE dimension_widgets SET avatarUrl = REPLACE(avatarUrl, '/img/', '/assets/img/')"
)
)
.then(() =>
queryInterface.sequelize.query(
"UPDATE dimension_bridges SET avatarUrl = REPLACE(avatarUrl, '/img/', '/assets/img/')"
)
);
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() =>
queryInterface.sequelize.query(
"UPDATE dimension_widgets SET avatarUrl = REPLACE(avatarUrl, '/assets/img/', '/img/')"
)
)
.then(() =>
queryInterface.sequelize.query(
"UPDATE dimension_bridges SET avatarUrl = REPLACE(avatarUrl, '/assets/img/', '/img/')"
)
);
},
};

View File

@ -5,7 +5,7 @@ import { Column, Model, PrimaryKey, Table } from "sequelize-typescript";
underscored: false,
timestamps: false,
})
export default class AppService extends Model<AppService> {
export default class AppService extends Model {
@PrimaryKey
@Column
id: string;

View File

@ -6,7 +6,7 @@ import AppService from "./AppService";
underscored: false,
timestamps: false,
})
export default class AppServiceUser extends Model<AppServiceUser> {
export default class AppServiceUser extends Model {
@PrimaryKey
@Column
id: string;

View File

@ -6,7 +6,7 @@ import { IntegrationRecord } from "./IntegrationRecord";
underscored: false,
timestamps: false,
})
export default class CustomSimpleBotRecord extends Model<CustomSimpleBotRecord> implements IntegrationRecord {
export default class CustomSimpleBotRecord extends Model implements IntegrationRecord {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import IrcBridgeRecord from "./IrcBridgeRecord";
underscored: false,
timestamps: false,
})
export default class IrcBridgeNetwork extends Model<IrcBridgeNetwork> {
export default class IrcBridgeNetwork extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import Upstream from "./Upstream";
underscored: false,
timestamps: false,
})
export default class IrcBridgeRecord extends Model<IrcBridgeRecord> {
export default class IrcBridgeRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -7,7 +7,7 @@ import NebIntegration from "./NebIntegration";
underscored: false,
timestamps: false,
})
export default class NebBotUser extends Model<NebBotUser> {
export default class NebBotUser extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -7,7 +7,7 @@ import AppService from "./AppService";
underscored: false,
timestamps: false,
})
export default class NebConfiguration extends Model<NebConfiguration> {
export default class NebConfiguration extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -7,7 +7,7 @@ import NebConfiguration from "./NebConfiguration";
underscored: false,
timestamps: false,
})
export default class NebIntegration extends Model<NebIntegration> implements IntegrationRecord {
export default class NebIntegration extends Model implements IntegrationRecord {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import NebIntegration from "./NebIntegration";
underscored: false,
timestamps: false,
})
export default class NebIntegrationConfig extends Model<NebIntegrationConfig> {
export default class NebIntegrationConfig extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -8,7 +8,7 @@ import User from "./User";
underscored: false,
timestamps: false,
})
export default class NebNotificationUser extends Model<NebNotificationUser> {
export default class NebNotificationUser extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import Upstream from "./Upstream";
underscored: false,
timestamps: false,
})
export default class SlackBridgeRecord extends Model<SlackBridgeRecord> {
export default class SlackBridgeRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import StickerPack from "./StickerPack";
underscored: false,
timestamps: false,
})
export default class Sticker extends Model<Sticker> {
export default class Sticker extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import { IntegrationRecord } from "./IntegrationRecord";
underscored: false,
timestamps: false,
})
export default class StickerPack extends Model<StickerPack> implements IntegrationRecord {
export default class StickerPack extends Model implements IntegrationRecord {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import Upstream from "./Upstream";
underscored: false,
timestamps: false,
})
export default class TelegramBridgeRecord extends Model<TelegramBridgeRecord> {
export default class TelegramBridgeRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import TermsTextRecord from "./TermsTextRecord";
underscored: false,
timestamps: false,
})
export default class TermsRecord extends Model<TermsRecord> {
export default class TermsRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -7,7 +7,7 @@ import TermsRecord from "./TermsRecord";
underscored: false,
timestamps: false,
})
export default class TermsSignedRecord extends Model<TermsSignedRecord> {
export default class TermsSignedRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -15,7 +15,7 @@ import TermsRecord from "./TermsRecord";
underscored: false,
timestamps: false,
})
export default class TermsTextRecord extends Model<TermsTextRecord> {
export default class TermsTextRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -15,7 +15,7 @@ import Upstream from "./Upstream";
underscored: false,
timestamps: false,
})
export default class TermsUpstreamRecord extends Model<TermsUpstreamRecord> {
export default class TermsUpstreamRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -5,7 +5,7 @@ import { AutoIncrement, Column, Model, PrimaryKey, Table } from "sequelize-types
underscored: false,
timestamps: false,
})
export default class Upstream extends Model<Upstream> {
export default class Upstream extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -5,7 +5,7 @@ import { Column, Model, PrimaryKey, Table } from "sequelize-typescript";
underscored: false,
timestamps: false,
})
export default class User extends Model<User> {
export default class User extends Model {
// This is really just a holding class to keep foreign keys alive
@PrimaryKey

View File

@ -16,7 +16,7 @@ import Upstream from "./Upstream";
underscored: false,
timestamps: false,
})
export default class UserScalarToken extends Model<UserScalarToken> {
export default class UserScalarToken extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -7,7 +7,7 @@ import User from "./User";
underscored: false,
timestamps: false,
})
export default class UserStickerPack extends Model<UserStickerPack> {
export default class UserStickerPack extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,7 +6,7 @@ import User from "./User";
underscored: false,
timestamps: false,
})
export default class Webhook extends Model<Webhook> {
export default class Webhook extends Model {
// This is really just a holding class to keep foreign keys alive
@PrimaryKey

View File

@ -6,7 +6,7 @@ import Upstream from "./Upstream";
underscored: false,
timestamps: false,
})
export default class WebhookBridgeRecord extends Model<WebhookBridgeRecord> {
export default class WebhookBridgeRecord extends Model {
@PrimaryKey
@AutoIncrement
@Column

View File

@ -6,12 +6,23 @@ import { CURRENT_VERSION } from "./version";
import { MatrixStickerBot } from "./matrix/MatrixStickerBot";
import * as BotSdk from "matrix-bot-sdk";
import User from "./db/models/User";
import { ILoggedInUser } from "./api/security/MatrixSecurity";
declare global {
namespace Express {
interface User extends ILoggedInUser {
userId: string;
token: string;
}
}
}
LogService.configure(config.logging);
LogService.info("index", "Starting dimension " + CURRENT_VERSION);
// Redirect the bot-sdk logger to our logger
BotSdk.LogService.setLogger({
trace: (module: string, ...args: any[]) => args.map(a => LogService.info("BotSdk-" + module, a)),
debug: (module: string, ...args: any[]) => args.map(a => LogService.info("BotSdk-" + module, a)),
info: (module: string, ...args: any[]) => args.map(a => LogService.info("BotSdk-" + module, a)),
warn: (module: string, ...args: any[]) => args.map(a => LogService.warn("BotSdk-" + module, a)),
@ -19,7 +30,8 @@ BotSdk.LogService.setLogger({
});
async function startup() {
await DimensionStore.updateSchema();
const schemas = await DimensionStore.updateSchema();
LogService.info("DimensionStore", schemas);
const webserver = new Webserver();
await webserver.start();

View File

@ -71,7 +71,7 @@ class _MatrixStickerBot {
authorName: await this.getUserId(),
authorReference: "https://matrix.to/#/" + (await this.getUserId()),
license: "Imported",
licensePath: "/licenses/general-imported.txt",
licensePath: "/assets/licenses/general-imported.txt",
trackingRoomAlias: alias,
});
return this.updateStickersInPacks([pack], roomId);

View File

@ -1,11 +1,11 @@
import * as dns from "dns-then";
import { promises as dnsPromises } from 'dns';
import { LogService } from "matrix-js-snippets";
import { Cache, CACHE_FEDERATION } from "../MemoryCache";
import * as request from "request";
import config from "../config";
import splitHost from 'split-host';
import * as isIP from "isipaddress";
import * as requestPromise from "request-promise";
import { isIP } from "net";
export interface IFederationConnectionInfo {
hostname: string;
@ -46,7 +46,7 @@ export async function getFederationConnInfo(serverName: string): Promise<IFedera
}
// Step 1 of the discovery process: if the hostname is an IP, use that with explicit or default port
if (isIP.test(hp.host)) {
if (isIP(hp.host) != 0) {
const fedUrl = `https://${hp.host}:${hp.port}`;
const fedObj = {url: fedUrl, hostname: serverName};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
@ -81,7 +81,7 @@ export async function getFederationConnInfo(serverName: string): Promise<IFedera
}
// Step 3a: if the delegated host is an IP address, use that (regardless of port)
if (isIP.test(wkHp.host)) {
if (isIP(wkHp.host) != 0) {
const fedUrl = `https://${wkHp.host}:${wkHp.port}`;
const fedObj = {url: fedUrl, hostname: wkServerAddr};
Cache.for(CACHE_FEDERATION).put(serverName, fedObj, expirationMs);
@ -100,7 +100,7 @@ export async function getFederationConnInfo(serverName: string): Promise<IFedera
// Step 3c: if the delegated host is not an IP and doesn't have a port, start a SRV lookup and use that
try {
const records = await dns.resolveSrv("_matrix._tcp." + hp.host);
const records = await dnsPromises.resolveSrv("_matrix._tcp." + hp.host);
if (records && records.length > 0) {
const fedUrl = `https://${records[0].name}:${records[0].port}`;
const fedObj = {url: fedUrl, hostname: wkHp.host};
@ -127,7 +127,7 @@ export async function getFederationConnInfo(serverName: string): Promise<IFedera
// Step 4: try resolving a hostname using SRV records and use that
try {
const records = await dns.resolveSrv("_matrix._tcp." + hp.host);
const records = await dnsPromises.resolveSrv("_matrix._tcp." + hp.host);
if (records && records.length > 0) {
const fedUrl = `https://${records[0].name}:${records[0].port}`;
const fedObj = {url: fedUrl, hostname: hp.host};

View File

@ -7,13 +7,13 @@ class _LicenseMap {
public readonly LICENSE_IMPORTED = "Imported";
private map: { [shortcode: string]: string } = {
"Imported": "/licenses/general-imported.txt",
"telegram": "/licenses/telegram-imported.txt",
"GPL v3.0": "/licenses/gpl-v3.0.txt",
"CC BY-NC-SA 4.0": "/licenses/cc_by-nc-sa_4.0.txt",
"CC BY-NC 4.0": "/licenses/cc_by-nc_4.0.txt",
"CC BY-SA 4.0": "/licenses/cc_by-sa_4.0.txt",
"CC BY 4.0": "/licenses/cc_by_4.0.txt",
"Imported": "/assets/licenses/general-imported.txt",
"telegram": "/assets/licenses/telegram-imported.txt",
"GPL v3.0": "/assets/licenses/gpl-v3.0.txt",
"CC BY-NC-SA 4.0": "/assets/licenses/cc_by-nc-sa_4.0.txt",
"CC BY-NC 4.0": "/assets/licenses/cc_by-nc_4.0.txt",
"CC BY-SA 4.0": "/assets/licenses/cc_by-sa_4.0.txt",
"CC BY 4.0": "/assets/licenses/cc_by_4.0.txt",
};
public find(name: string): License {

View File

@ -1,6 +1,6 @@
import Telegraf, { Telegram } from "telegraf";
import config from "../config";
import * as path from "path";
import { Telegraf, Telegram } from "telegraf";
export interface TelegramStickerPack {
name: string;
@ -30,7 +30,7 @@ export class TelegramBot {
for (const tgSticker of set.stickers) {
pack.stickers.push({
url: await this.bot.getFileLink(tgSticker.file_id),
url: await (await this.bot.getFileLink(tgSticker.file_id)).toString(),
emoji: tgSticker.emoji,
});
}

View File

@ -1,17 +1,19 @@
import * as git from "git-rev-sync";
import * as child_process from 'child_process';
let version = "Unknown";
let gitHash = null;
try {
version = "v" + require("../../package.json").version;
version = "v" + require("../package.json").version;
} catch (error) {
// The log service isn't set up by the time we require this file
console.error("version", error);
}
try {
gitHash = git.short();
gitHash = child_process
.execSync('git rev-parse --short HEAD')
.toString().trim()
} catch (error) {
// The log service isn't set up by the time we require this file
console.error("version", error);

View File

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

15
tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"baseUrl": "./web",
"outDir": "./build/web",
"types": ["bluebird", "body-parser", "jquery", "validator"]
},
"files": [
"web/main.ts",
"web/polyfills.ts"
],
"include": [
"web/**/*.d.ts"
]
}

19
tsconfig.backend.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"baseUrl": "./src",
"outDir": "./build/app",
"module": "CommonJS",
"types": ["bluebird", "body-parser", "jquery", "validator"],
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"sourceMap": false,
"target": "es2015",
},
"include": [
"./src/**/*"
],
}

View File

@ -1,19 +1,38 @@
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noEmitHelpers": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"lib": ["es2015", "dom"]
},
"compileOnSave": false,
"buildOnSave": false,
"awesomeTypescriptLoaderOptions": {
"forkChecker": true,
"useWebpackText": true
}
}
"compilerOptions": {
"baseUrl": "./",
"outDir": "./build",
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictNullChecks": false, // TODO: Should be fixed
"noImplicitAny": false, // TODO: Should be fixed
"noImplicitReturns": false, // TODO: Should be fixed
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictDomEventTypes": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictPropertyInitialization": false,
"strictTemplates": true,
"types" : ["node"]
},
"exclude":[
"./node_modules"
]
}

16
tsconfig.spec.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine", "bluebird", "body-parser", "jquery", "validator"]
},
"files": [
"web/test.ts",
"web/polyfills.ts"
],
"include": [
"web/**/*.spec.ts",
"web/**/*.d.ts"
]
}

View File

@ -1,111 +0,0 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"class-name": false,
"comment-format": [
true
],
"curly": false,
"eofline": false,
"forin": false,
"indent": [
true,
"spaces"
],
"label-position": true,
"max-line-length": false,
"member-access": false,
"member-ordering": [
true,
"static-after-instance",
"variables-before-functions"
],
"no-arg": true,
"no-bitwise": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-inferrable-types": true,
"no-shadowed-variable": false,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-use-before-declare": false,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": false,
"radix": true,
"semicolon": [
"always"
],
"triple-equals": [],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"my",
"camelCase"
],
"component-selector": [
true,
"element",
"my",
"kebab-case"
],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"pipe-naming": [
true,
"camelCase",
"my"
],
"no-attribute-parameter-decorator": true,
"no-forward-ref": true,
"import-destructuring-spacing": true
}
}

View File

@ -14,7 +14,7 @@ export class AdminBridgesComponent implements OnInit {
public bridges: FE_Bridge<any>[];
constructor(private adminIntegrations: AdminIntegrationsApiService,
private toaster: ToasterService, public translate: TranslateService) {
private toaster: ToasterService, public translate: TranslateService) {
this.translate = translate;
}
@ -24,7 +24,9 @@ export class AdminBridgesComponent implements OnInit {
this.isLoading = false;
}).catch(err => {
console.error(err);
this.translate.get('Failed to load bridges').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to load bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,24 +1,23 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'Add a new self-hosted IRC Bridge' | translate}}</h4>
</div>
<div class="dialog-content">
<p>{{'Self-hosted IRC bridges must have' | translate}}<code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9999"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
</div>
<div class="dialog-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'Add a new self-hosted IRC Bridge' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{'Self-hosted IRC bridges must have' | translate}}<code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is usually the same as the URL given in the registration. This API is not authenticated and should be treated with caution.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9999"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,38 +1,43 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { AdminIrcApiService } from "../../../../shared/services/admin/admin-irc-api.service";
import { TranslateService } from "@ngx-translate/core";
export class AddSelfhostedIrcBridgeDialogContext extends BSModalContext {
export interface AddSelfhostedIrcBridgeDialogContextt {
isSaving: boolean;
provisionUrl: string;
}
@Component({
templateUrl: "./add-selfhosted.component.html",
styleUrls: ["./add-selfhosted.component.scss"],
})
export class AdminIrcBridgeAddSelfhostedComponent implements ModalComponent<AddSelfhostedIrcBridgeDialogContext> {
export class AdminIrcBridgeAddSelfhostedComponent {
public isSaving = false;
public provisionUrl: string;
constructor(public dialog: DialogRef<AddSelfhostedIrcBridgeDialogContext>,
private ircApi: AdminIrcApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private ircApi: AdminIrcApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
}
public add() {
this.isSaving = true;
this.ircApi.newSelfhosted(this.provisionUrl).then(() => {
this.translate.get('IRC Bridge added').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('IRC Bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create IRC bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to create IRC bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -4,13 +4,12 @@ import { AdminIrcApiService } from "../../../shared/services/admin/admin-irc-api
import { FE_Upstream } from "../../../shared/models/admin-responses";
import { AdminUpstreamApiService } from "../../../shared/services/admin/admin-upstream-api.service";
import { FE_IrcBridge } from "../../../shared/models/irc";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import { AdminIrcBridgeNetworksComponent, IrcNetworksDialogContext } from "./networks/networks.component";
import {
AddSelfhostedIrcBridgeDialogContext,
AdminIrcBridgeAddSelfhostedComponent
} from "./add-selfhosted/add-selfhosted.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
templateUrl: "./irc.component.html",
@ -26,10 +25,10 @@ export class AdminIrcBridgeComponent implements OnInit {
private upstreams: FE_Upstream[];
constructor(private upstreamApi: AdminUpstreamApiService,
private ircApi: AdminIrcApiService,
private toaster: ToasterService,
private modal: Modal,
public translate: TranslateService) {
private ircApi: AdminIrcApiService,
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
@ -51,7 +50,9 @@ export class AdminIrcBridgeComponent implements OnInit {
}
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
@ -70,13 +71,17 @@ export class AdminIrcBridgeComponent implements OnInit {
const createBridge = (upstream: FE_Upstream) => {
return this.ircApi.newFromUpstream(upstream).then(bridge => {
this.configurations.push(bridge);
this.translate.get(['Click the pencil icon to enable networks.', 'matrix.org\'s IRC bridge added']).subscribe((res: string) => {this.toaster.pop("success", res[0], res[1]); });
this.translate.get(['Click the pencil icon to enable networks.', 'matrix.org\'s IRC bridge added']).subscribe((res: string) => {
this.toaster.pop("success", res[0], res[1]);
});
this.isUpdating = false;
this.hasModularBridge = true;
}).catch(err => {
console.error(err);
this.isUpdating = false;
this.translate.get('Error adding matrix.org\'s IRC Bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error adding matrix.org\'s IRC Bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
};
const vectorUpstreams = this.upstreams.filter(u => u.type === "vector");
@ -88,29 +93,46 @@ export class AdminIrcBridgeComponent implements OnInit {
createBridge(upstream);
}).catch(err => {
console.error(err);
this.translate.get('Error creating matrix.org\'s IRC Bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error creating matrix.org\'s IRC Bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else createBridge(vectorUpstreams[0]);
}
public addSelfHostedBridge() {
this.modal.open(AdminIrcBridgeAddSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminIrcBridgeAddSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
}, AddSelfhostedIrcBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update IRC bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update IRC bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
}
public editNetworks(bridge: FE_IrcBridge) {
this.modal.open(AdminIrcBridgeNetworksComponent, overlayConfigFactory({
bridge: bridge,
isBlocking: true,
const selfhostedRef = this.modal.open(AdminIrcBridgeNetworksComponent, {
backdrop: 'static',
size: 'lg',
}, IrcNetworksDialogContext));
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update IRC bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as IrcNetworksDialogContext;
selfhostedInstance.bridge = bridge;
}
}

View File

@ -1,29 +1,28 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{ bridge.upstreamId ? "matrix.org's" : "Self-hosted" }} IRC Bridge Networks</h4>
</div>
<div class="dialog-content">
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>{{'Network' | translate}}</th>
<th>{{'Enabled' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let network of networks trackById">
<td>{{ network.name }}</td>
<td>
<ui-switch [checked]="network.isEnabled" size="small" [disabled]="isUpdating"
(change)="toggleNetwork(network)"></ui-switch>
</td>
</tr>
</tbody>
</table>
</div>
<div class="dialog-footer">
<button type="button" (click)="dialog.close()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-times-circle"></i> {{'Close' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{ bridge.upstreamId ? "matrix.org's" : "Self-hosted" }} IRC Bridge Networks</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<table class="table table-striped table-condensed table-bordered">
<thead>
<tr>
<th>{{'Network' | translate}}</th>
<th>{{'Enabled' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let network of networks trackById">
<td>{{ network.name }}</td>
<td>
<ui-switch [checked]="network.isEnabled" size="small" [disabled]="isUpdating"
(change)="toggleNetwork(network)"></ui-switch>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" (click)="modal.close()" title="close" class="btn btn-outline-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Close' | translate}}
</button>
</div>

View File

@ -1,13 +1,12 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { FE_IrcBridge } from "../../../../shared/models/irc";
import { AdminIrcApiService } from "../../../../shared/services/admin/admin-irc-api.service";
import { TranslateService } from "@ngx-translate/core";
export class IrcNetworksDialogContext extends BSModalContext {
public bridge: FE_IrcBridge;
export interface IrcNetworksDialogContext {
bridge: FE_IrcBridge;
}
interface LocalNetwork {
@ -22,19 +21,17 @@ interface LocalNetwork {
templateUrl: "./networks.component.html",
styleUrls: ["./networks.component.scss"],
})
export class AdminIrcBridgeNetworksComponent implements ModalComponent<IrcNetworksDialogContext> {
export class AdminIrcBridgeNetworksComponent {
public isUpdating = false;
public bridge: FE_IrcBridge;
public networks: LocalNetwork[];
constructor(public dialog: DialogRef<IrcNetworksDialogContext>,
private ircApi: AdminIrcApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private ircApi: AdminIrcApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.bridge = dialog.context.bridge;
const networkIds = Object.keys(this.bridge.availableNetworks);
this.networks = networkIds.map(i => {
return {
@ -54,13 +51,17 @@ export class AdminIrcBridgeNetworksComponent implements ModalComponent<IrcNetwor
this.isUpdating = true;
this.ircApi.setNetworkEnabled(this.bridge.id, network.id, network.isEnabled).then(() => {
this.isUpdating = false;
this.translate.get(['Enabled', 'disabled']).subscribe((res: string) => {this.toaster.pop("success", "Network " + (network.isEnabled ? res[0] : res[1])); });
this.translate.get(['Enabled', 'disabled']).subscribe((res: string) => {
this.toaster.pop("success", "Network " + (network.isEnabled ? res[0] : res[1]));
});
}).catch(err => {
console.error(err);
this.isUpdating = false;
network.isEnabled = !network.isEnabled;
this.bridge.availableNetworks[network.id].isEnabled = network.isEnabled;
this.translate.get('Failed to update network').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to update network').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,26 +1,25 @@
<div class="dialog">
<div class="dialog-header">
<h4> {{'self-hosted Slack bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</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.' | translate}}
</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9000"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
</div>
<div class="dialog-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted Slack bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>
{{'Self-hosted Slack bridges already have provisioning enabled. Be careful not to expose the API to the public internet.' | translate}}
</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is usually the same as the URL your homeserver uses to communicate with the bridge.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9000"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,55 +1,58 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { AdminSlackApiService } from "../../../../shared/services/admin/admin-slack-api.service";
import { TranslateService } from "@ngx-translate/core";
export class ManageSelfhostedSlackBridgeDialogContext extends BSModalContext {
public provisionUrl: string;
public bridgeId: number;
export interface ManageSelfhostedSlackBridgeDialogContext {
provisionUrl: string;
bridgeId: number;
isAdding: boolean;
}
@Component({
templateUrl: "./manage-selfhosted.component.html",
styleUrls: ["./manage-selfhosted.component.scss"],
})
export class AdminSlackBridgeManageSelfhostedComponent implements ModalComponent<ManageSelfhostedSlackBridgeDialogContext> {
export class AdminSlackBridgeManageSelfhostedComponent {
public isSaving = false;
public provisionUrl: string;
public bridgeId: number;
public isAdding = false;
public isAdding = true;
constructor(public dialog: DialogRef<ManageSelfhostedSlackBridgeDialogContext>,
private slackApi: AdminSlackApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private slackApi: AdminSlackApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.provisionUrl = dialog.context.provisionUrl;
this.bridgeId = dialog.context.bridgeId;
this.isAdding = !this.bridgeId;
}
public add() {
this.isSaving = true;
if (this.isAdding) {
this.slackApi.newSelfhosted(this.provisionUrl).then(() => {
this.translate.get('Slack bridge added').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Slack bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create Slack bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to create Slack bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else {
this.slackApi.updateSelfhosted(this.bridgeId, this.provisionUrl).then(() => {
this.translate.get('Slack bridge updated').subscribe((res: string) => this.toaster.pop("success", res));
this.dialog.close();
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to update Slack bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to update Slack bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,6 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import { FE_Upstream } from "../../../shared/models/admin-responses";
import { AdminUpstreamApiService } from "../../../shared/services/admin/admin-upstream-api.service";
import {
@ -10,6 +9,7 @@ import {
import { FE_SlackBridge } from "../../../shared/models/slack";
import { AdminSlackApiService } from "../../../shared/services/admin/admin-slack-api.service";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
templateUrl: "./slack.component.html",
@ -24,10 +24,10 @@ export class AdminSlackBridgeComponent implements OnInit {
private upstreams: FE_Upstream[];
constructor(private slackApi: AdminSlackApiService,
private upstreamApi: AdminUpstreamApiService,
private toaster: ToasterService,
private modal: Modal,
public translate: TranslateService) {
private upstreamApi: AdminUpstreamApiService,
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
@ -41,7 +41,9 @@ export class AdminSlackBridgeComponent implements OnInit {
this.configurations = await this.slackApi.getBridges();
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
@ -51,12 +53,16 @@ export class AdminSlackBridgeComponent implements OnInit {
const createBridge = (upstream: FE_Upstream) => {
return this.slackApi.newFromUpstream(upstream).then(bridge => {
this.configurations.push(bridge);
this.translate.get('matrix.org\'s Slack bridge added').subscribe((res: string) => {this.toaster.pop("success", res); });
this.translate.get('matrix.org\'s Slack bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.isUpdating = false;
}).catch(err => {
console.error(err);
this.isUpdating = false;
this.translate.get('Error adding matrix.org\'s Slack Bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error adding matrix.org\'s Slack Bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
};
@ -69,37 +75,50 @@ export class AdminSlackBridgeComponent implements OnInit {
createBridge(upstream);
}).catch(err => {
console.error(err);
this.translate.get('Error creating matrix.org\'s Slack Bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error creating matrix.org\'s Slack Bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else createBridge(vectorUpstreams[0]);
}
public addSelfHostedBridge() {
this.modal.open(AdminSlackBridgeManageSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminSlackBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
provisionUrl: '',
}, ManageSelfhostedSlackBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update Slack bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update Slack bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedSlackBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
}
public editBridge(bridge: FE_SlackBridge) {
this.modal.open(AdminSlackBridgeManageSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminSlackBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
provisionUrl: bridge.provisionUrl,
bridgeId: bridge.id,
}, ManageSelfhostedSlackBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update Slack bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update Slack bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedSlackBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
selfhostedInstance.bridgeId = bridge.id;
selfhostedInstance.isAdding = !bridge.id;
}
}

View File

@ -1,46 +1,45 @@
<div class="dialog">
<div class="dialog-header">
<h4> {{'self-hosted Telegram bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
</div>
<div class="dialog-content">
<p>{{'Self-hosted Telegram bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9999/_matrix/provision/v1"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The shared secret defined in the configuration for provisioning.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Promote Telegram Puppeting' | translate}}
<span class="text-muted ">{{'If enabled, Dimension will recommend that users log in to their Telegram accounts.' | translate}}</span>
<ui-switch [checked]="allowTgPuppets" size="small" [disabled]="isSaving"
(change)="allowTgPuppets = !allowTgPuppets"></ui-switch>
</label>
<label class="label-block">
{{'Promote Matrix Puppeting' | translate}}
<span class="text-muted ">{{'If enabled, Dimension will recommend that users log in to their Matrix accounts.' | translate}}</span>
<ui-switch [checked]="allowMxPuppets" size="small" [disabled]="isSaving"
(change)="allowMxPuppets = !allowMxPuppets"></ui-switch>
</label>
</div>
<div class="dialog-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted Telegram bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{'Self-hosted Telegram bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The provisioning URL for the bridge. This is the public address for the bridge followed by the provisioning prefix given in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="http://localhost:9999/_matrix/provision/v1"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The shared secret defined in the configuration for provisioning.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Promote Telegram Puppeting' | translate}}
<span class="text-muted ">{{'If enabled, Dimension will recommend that users log in to their Telegram accounts.' | translate}}</span>
<ui-switch [checked]="allowTgPuppets" size="small" [disabled]="isSaving"
(change)="allowTgPuppets = !allowTgPuppets"></ui-switch>
</label>
<label class="label-block">
{{'Promote Matrix Puppeting' | translate}}
<span class="text-muted ">{{'If enabled, Dimension will recommend that users log in to their Matrix accounts.' | translate}}</span>
<ui-switch [checked]="allowMxPuppets" size="small" [disabled]="isSaving"
(change)="allowMxPuppets = !allowMxPuppets"></ui-switch>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,43 +1,37 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { AdminTelegramApiService } from "../../../../shared/services/admin/admin-telegram-api.service";
import { TranslateService } from "@ngx-translate/core";
export class ManageSelfhostedTelegramBridgeDialogContext extends BSModalContext {
public provisionUrl: string;
public sharedSecret: string;
public allowTgPuppets = false;
public allowMxPuppets = false;
public bridgeId: number;
export interface ManageSelfhostedTelegramBridgeDialogContext {
provisionUrl: string;
sharedSecret: string;
allowTgPuppets: boolean;
allowMxPuppets: boolean;
bridgeId: number;
isAdding: boolean;
}
@Component({
templateUrl: "./manage-selfhosted.component.html",
styleUrls: ["./manage-selfhosted.component.scss"],
})
export class AdminTelegramBridgeManageSelfhostedComponent implements ModalComponent<ManageSelfhostedTelegramBridgeDialogContext> {
export class AdminTelegramBridgeManageSelfhostedComponent {
public isSaving = false;
public provisionUrl: string;
public sharedSecret: string;
public allowTgPuppets = false;
public allowMxPuppets = false;
public bridgeId: number;
public isAdding = false;
isSaving = false;
provisionUrl: string;
sharedSecret: string;
allowTgPuppets = false;
allowMxPuppets = false;
bridgeId: number;
isAdding = true;
constructor(public dialog: DialogRef<ManageSelfhostedTelegramBridgeDialogContext>,
private telegramApi: AdminTelegramApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private telegramApi: AdminTelegramApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.provisionUrl = dialog.context.provisionUrl;
this.sharedSecret = dialog.context.sharedSecret;
this.allowTgPuppets = dialog.context.allowTgPuppets;
this.allowMxPuppets = dialog.context.allowMxPuppets;
this.bridgeId = dialog.context.bridgeId;
this.isAdding = !this.bridgeId;
}
public add() {
@ -48,21 +42,29 @@ export class AdminTelegramBridgeManageSelfhostedComponent implements ModalCompon
};
if (this.isAdding) {
this.telegramApi.newSelfhosted(this.provisionUrl, this.sharedSecret, options).then(() => {
this.translate.get('Telegram bridge added').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Telegram bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create Telegram bridge').subscribe((res: string) => { this.toaster.pop("error", res); });
this.translate.get('Failed to create Telegram bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else {
this.telegramApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret, options).then(() => {
this.translate.get('Telegram bridge updated').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Telegram bridge updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to update Telegram bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to update Telegram bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,6 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import {
AdminTelegramBridgeManageSelfhostedComponent,
ManageSelfhostedTelegramBridgeDialogContext
@ -8,6 +7,7 @@ import {
import { FE_TelegramBridge } from "../../../shared/models/telegram";
import { AdminTelegramApiService } from "../../../shared/services/admin/admin-telegram-api.service";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
templateUrl: "./telegram.component.html",
@ -20,9 +20,9 @@ export class AdminTelegramBridgeComponent implements OnInit {
public configurations: FE_TelegramBridge[] = [];
constructor(private telegramApi: AdminTelegramApiService,
private toaster: ToasterService,
private modal: Modal,
public translate: TranslateService) {
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
@ -35,24 +35,32 @@ export class AdminTelegramBridgeComponent implements OnInit {
this.configurations = await this.telegramApi.getBridges();
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
public addSelfHostedBridge() {
this.modal.open(AdminTelegramBridgeManageSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminTelegramBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
provisionUrl: '',
sharedSecret: '',
allowPuppets: false,
}, ManageSelfhostedTelegramBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update Telegram bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update Telegram bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedTelegramBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
selfhostedInstance.sharedSecret = '';
selfhostedInstance.allowMxPuppets = false;
selfhostedInstance.allowTgPuppets = false;
}
public getEnabledFeaturesString(bridge: FE_TelegramBridge): string {
@ -63,20 +71,26 @@ export class AdminTelegramBridgeComponent implements OnInit {
}
public editBridge(bridge: FE_TelegramBridge) {
this.modal.open(AdminTelegramBridgeManageSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminTelegramBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
provisionUrl: bridge.provisionUrl,
sharedSecret: bridge.sharedSecret,
allowTgPuppets: bridge.options ? bridge.options.allowTgPuppets : false,
allowMxPuppets: bridge.options ? bridge.options.allowMxPuppets : false,
bridgeId: bridge.id,
}, ManageSelfhostedTelegramBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update Telegram bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update Telegram bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
})
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedTelegramBridgeDialogContext;
selfhostedInstance.provisionUrl = bridge.provisionUrl;
selfhostedInstance.sharedSecret = bridge.sharedSecret;
selfhostedInstance.allowMxPuppets = bridge.options?.allowTgPuppets || false;
selfhostedInstance.allowTgPuppets = bridge.options?.allowMxPuppets || false;
selfhostedInstance.bridgeId = bridge.id;
selfhostedInstance.isAdding = !bridge.id;
}
}

View File

@ -1,32 +1,31 @@
<div class="dialog">
<div class="dialog-header">
<h4> {{'self-hosted webhook bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
</div>
<div class="dialog-content">
<p>{{'Self-hosted webhook bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The public URL for the bridge.' | translate}}</span>
<input type="text" class="form-control"
placeholder="https://webhooks.example.org:9000/"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The provisioning secret defined in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
</div>
<div class="dialog-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'self-hosted webhook bridge' | translate}} ({{ isAdding ? "Add a new" : "Edit" }})</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<p>{{'Self-hosted webhook bridges must have' | translate}} <code>{{'provisioning' | translate}}</code> {{'enabled in the configuration.' | translate}}</p>
<label class="label-block">
{{'Provisioning URL' | translate}}
<span class="text-muted ">{{'The public URL for the bridge.' | translate}}</span>
<input type="text" class="form-control"
placeholder="https://webhooks.example.org:9000/"
[(ngModel)]="provisionUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Shared Secret' | translate}}
<span class="text-muted ">{{'The provisioning secret defined in the configuration.' | translate}}</span>
<input type="text" class="form-control"
placeholder="some_secret_value"
[(ngModel)]="sharedSecret" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,60 +1,66 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { AdminWebhooksApiService } from "../../../../shared/services/admin/admin-webhooks-api.service";
import { TranslateService } from "@ngx-translate/core";
export class ManageSelfhostedWebhooksBridgeDialogContext extends BSModalContext {
public provisionUrl: string;
public sharedSecret: string;
public allowTgPuppets = false;
public allowMxPuppets = false;
public bridgeId: number;
export interface ManageSelfhostedWebhooksBridgeDialogContext {
provisionUrl: string;
sharedSecret: string;
allowTgPuppets: boolean;
allowMxPuppets: boolean;
bridgeId: number;
isAdding: boolean;
}
@Component({
templateUrl: "./manage-selfhosted.component.html",
styleUrls: ["./manage-selfhosted.component.scss"],
})
export class AdminWebhooksBridgeManageSelfhostedComponent implements ModalComponent<ManageSelfhostedWebhooksBridgeDialogContext> {
export class AdminWebhooksBridgeManageSelfhostedComponent {
public isSaving = false;
public provisionUrl: string;
public sharedSecret: string;
public provisionUrl: '';
public sharedSecret: '';
public allowTgPuppets = false;
public allowMxPuppets = false;
public bridgeId: number;
public isAdding = false;
public isAdding = true;
constructor(public dialog: DialogRef<ManageSelfhostedWebhooksBridgeDialogContext>,
private webhooksApi: AdminWebhooksApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private webhooksApi: AdminWebhooksApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.provisionUrl = dialog.context.provisionUrl;
this.sharedSecret = dialog.context.sharedSecret;
this.bridgeId = dialog.context.bridgeId;
this.isAdding = !this.bridgeId;
}
public add() {
this.isSaving = true;
if (this.isAdding) {
this.webhooksApi.newSelfhosted(this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Webhook bridge added').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Webhook bridge added').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to create Webhook bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to create Webhook bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
} else {
this.webhooksApi.updateSelfhosted(this.bridgeId, this.provisionUrl, this.sharedSecret).then(() => {
this.translate.get('Webhook bridge updated').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Webhook bridge updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Failed to update Webhook bridge').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Failed to update Webhook bridge').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,6 +1,5 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import {
AdminWebhooksBridgeManageSelfhostedComponent,
ManageSelfhostedWebhooksBridgeDialogContext
@ -8,6 +7,7 @@ import {
import { FE_WebhooksBridge } from "../../../shared/models/webhooks";
import { AdminWebhooksApiService } from "../../../shared/services/admin/admin-webhooks-api.service";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
templateUrl: "./webhooks.component.html",
@ -20,9 +20,9 @@ export class AdminWebhooksBridgeComponent implements OnInit {
public configurations: FE_WebhooksBridge[] = [];
constructor(private webhooksApi: AdminWebhooksApiService,
private toaster: ToasterService,
private modal: Modal,
public translate: TranslateService) {
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
@ -35,39 +35,53 @@ export class AdminWebhooksBridgeComponent implements OnInit {
this.configurations = await this.webhooksApi.getBridges();
} catch (err) {
console.error(err);
this.translate.get('Error loading bridges').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading bridges').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
}
public addSelfHostedBridge() {
this.modal.open(AdminWebhooksBridgeManageSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminWebhooksBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
provisionUrl: '',
sharedSecret: '',
allowPuppets: false,
}, ManageSelfhostedWebhooksBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update Webhooks bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update Webhooks bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedWebhooksBridgeDialogContext;
selfhostedInstance.provisionUrl = '';
selfhostedInstance.sharedSecret = '';
selfhostedInstance.allowMxPuppets = false;
selfhostedInstance.allowTgPuppets = false;
}
public editBridge(bridge: FE_WebhooksBridge) {
this.modal.open(AdminWebhooksBridgeManageSelfhostedComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminWebhooksBridgeManageSelfhostedComponent, {
backdrop: 'static',
size: 'lg',
provisionUrl: bridge.provisionUrl,
sharedSecret: bridge.sharedSecret,
bridgeId: bridge.id,
}, ManageSelfhostedWebhooksBridgeDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an update Webhooks bridge list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an update Webhooks bridge list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
const selfhostedInstance = selfhostedRef.componentInstance as ManageSelfhostedWebhooksBridgeDialogContext;
selfhostedInstance.provisionUrl = bridge.provisionUrl;
selfhostedInstance.sharedSecret = bridge.sharedSecret
selfhostedInstance.bridgeId = bridge.id;
selfhostedInstance.isAdding = !bridge.id;
}
}

View File

@ -1,56 +1,55 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{ isAdding ? "Add a new" : "Edit" }} {{ 'custom bot' | translate}}</h4>
</div>
<div class="dialog-content">
<label class="label-block">
User ID
<span class="text-muted">{{'The user ID that Dimension will invite to rooms.' | translate}}</span>
<input type="text" class="form-control"
placeholder="@yourbot:example.org"
[(ngModel)]="bot.userId" [disabled]="isSaving" (blur)="loadProfile()"/>
</label>
<label class="label-block">
Description
<span class="text-muted ">{{'A few words here will help people understand what the bot does.' | translate}}</span>
<input type="text" class="form-control"
placeholder="Does awesome things"
[(ngModel)]="bot.description" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Display Name' | translate}}
<span class="text-muted ">{{'This is the name Dimension will use to tell users which bot this is.' | translate}}</span>
<input type="text" class="form-control"
placeholder="Cool Bot"
[(ngModel)]="bot.name" [disabled]="isSaving"/>
</label>
<label class="label-block">
Avatar URL
<span class="text-muted ">{{'This can either be an MXC URI or a plain URL.' | translate}}</span>
<input type="text" class="form-control"
placeholder="mxc://example.org/C00lAvat4r"
[(ngModel)]="bot.avatarUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
Access Token
<span class="text-muted ">{{'This is used by Dimension to force the bot to leave the room when the user removes the bot.' | translate}}
<a href="https://t2bot.io/docs/access_tokens/" target="_blank">{{'Learn more about access tokens.' | translate}}</a>.
</span>
<input type="text" class="form-control"
placeholder="MDaX..."
[(ngModel)]="bot.accessToken" [disabled]="isSaving"/>
</label>
</div>
<div class="dialog-footer">
<button type="button" (click)="add()" title="close" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i>{{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{ isAdding ? "Add a new" : "Edit" }} {{ 'custom bot' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<label class="label-block">
User ID
<span class="text-muted">{{'The user ID that Dimension will invite to rooms.' | translate}}</span>
<input type="text" class="form-control"
placeholder="@yourbot:example.org"
[(ngModel)]="bot.userId" [disabled]="isSaving" (blur)="loadProfile()"/>
</label>
<label class="label-block">
Description
<span class="text-muted ">{{'A few words here will help people understand what the bot does.' | translate}}</span>
<input type="text" class="form-control"
placeholder="Does awesome things"
[(ngModel)]="bot.description" [disabled]="isSaving"/>
</label>
<label class="label-block">
{{'Display Name' | translate}}
<span class="text-muted ">{{'This is the name Dimension will use to tell users which bot this is.' | translate}}</span>
<input type="text" class="form-control"
placeholder="Cool Bot"
[(ngModel)]="bot.name" [disabled]="isSaving"/>
</label>
<label class="label-block">
Avatar URL
<span class="text-muted ">{{'This can either be an MXC URI or a plain URL.' | translate}}</span>
<input type="text" class="form-control"
placeholder="mxc://example.org/C00lAvat4r"
[(ngModel)]="bot.avatarUrl" [disabled]="isSaving"/>
</label>
<label class="label-block">
Access Token
<span class="text-muted ">{{'This is used by Dimension to force the bot to leave the room when the user removes the bot.' | translate}}
<a href="https://t2bot.io/docs/access_tokens/" target="_blank">{{'Learn more about access tokens.' | translate}}</a>.
</span>
<input type="text" class="form-control"
placeholder="MDaX..."
[(ngModel)]="bot.accessToken" [disabled]="isSaving"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="add()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,34 +1,32 @@
import { Component } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { FE_CustomSimpleBot, FE_UserProfile } from "../../../shared/models/admin-responses";
import { AdminCustomSimpleBotsApiService } from "../../../shared/services/admin/admin-custom-simple-bots-api.service";
import { TranslateService } from "@ngx-translate/core";
export class AddCustomBotDialogContext extends BSModalContext {
export interface AddCustomBotDialogContext {
bot: FE_CustomSimpleBot;
isAdding: boolean;
}
@Component({
templateUrl: "./add.component.html",
styleUrls: ["./add.component.scss"],
})
export class AdminAddCustomBotComponent implements ModalComponent<AddCustomBotDialogContext> {
export class AdminAddCustomBotComponent {
public bot: FE_CustomSimpleBot;
public isAdding = false;
public bot = <FE_CustomSimpleBot>{};
public isAdding = true;
public isSaving = false;
private lastProfile: FE_UserProfile;
constructor(public dialog: DialogRef<AddCustomBotDialogContext>,
private botApi: AdminCustomSimpleBotsApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private botApi: AdminCustomSimpleBotsApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.bot = this.dialog.context.bot || <FE_CustomSimpleBot>{};
this.isAdding = !this.dialog.context.bot;
}
public loadProfile() {
@ -48,23 +46,33 @@ export class AdminAddCustomBotComponent implements ModalComponent<AddCustomBotDi
public add() {
if (!this.bot.name) {
this.translate.get('Please enter a name for the bot').subscribe((res: string) => {this.toaster.pop("warning", res); });
this.translate.get('Please enter a name for the bot').subscribe((res: string) => {
this.toaster.pop("warning", res);
});
return;
}
if (!this.bot.avatarUrl) {
this.translate.get('Please enter an avatar URL for the bot').subscribe((res: string) => {this.toaster.pop("warning", res); });
this.translate.get('Please enter an avatar URL for the bot').subscribe((res: string) => {
this.toaster.pop("warning", res);
});
return;
}
if (!this.bot.userId) {
this.translate.get('Please enter a user ID for the bot').subscribe((res: string) => {this.toaster.pop("warning", res); });
this.translate.get('Please enter a user ID for the bot').subscribe((res: string) => {
this.toaster.pop("warning", res);
});
return;
}
if (!this.bot.description) {
this.translate.get('Please enter a description for the bot').subscribe((res: string) => {this.toaster.pop("warning", res); });
this.translate.get('Please enter a description for the bot').subscribe((res: string) => {
this.toaster.pop("warning", res);
});
return;
}
if (!this.bot.accessToken) {
this.translate.get('Please enter an access token for the bot').subscribe((res: string) => {this.toaster.pop("warning", res); });
this.translate.get('Please enter an access token for the bot').subscribe((res: string) => {
this.toaster.pop("warning", res);
});
return;
}
@ -88,12 +96,16 @@ export class AdminAddCustomBotComponent implements ModalComponent<AddCustomBotDi
}
promise.then(() => {
this.translate.get('Bot updated').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Bot updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(error => {
this.isSaving = false;
console.error(error);
this.translate.get('Error updating bot').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error updating bot').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -2,9 +2,9 @@ import { Component } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { FE_CustomSimpleBot } from "../../shared/models/admin-responses";
import { AdminCustomSimpleBotsApiService } from "../../shared/services/admin/admin-custom-simple-bots-api.service";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import { AddCustomBotDialogContext, AdminAddCustomBotComponent } from "./add/add.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
templateUrl: "./custom-bots.component.html",
@ -17,14 +17,16 @@ export class AdminCustomBotsComponent {
public isUpdating = false;
constructor(private botApi: AdminCustomSimpleBotsApiService,
private toaster: ToasterService,
private modal: Modal,
public translate: TranslateService) {
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
this.reload().then(() => this.isLoading = false).catch(error => {
console.error(error);
this.translate.get('Error loading go-neb configuration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading go-neb configuration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
@ -36,29 +38,40 @@ export class AdminCustomBotsComponent {
}
public addBot() {
this.modal.open(AdminAddCustomBotComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminAddCustomBotComponent, {
backdrop: 'static',
size: 'lg',
}, AddCustomBotDialogContext)).result.then(() => {
this.reload().catch(err => {
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated bot list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
this.translate.get('Failed to get an updated bot list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
}
public editBot(bot: FE_CustomSimpleBot) {
this.modal.open(AdminAddCustomBotComponent, overlayConfigFactory({
isBlocking: true,
const selfhostedRef = this.modal.open(AdminAddCustomBotComponent, {
backdrop: 'static',
size: 'lg',
bot: bot,
}, AddCustomBotDialogContext)).result.then(() => {
this.reload().catch(err => {
console.error(err);
this.translate.get('Failed to get an updated bot list').subscribe((res: string) => {this.toaster.pop("error", res); });
});
});
selfhostedRef.result.then(() => {
try {
this.reload()
} catch (err) {
console.error(err);
this.translate.get('Failed to get an updated bot list').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
const selfhostedInstance = selfhostedRef.componentInstance as AddCustomBotDialogContext;
selfhostedInstance.bot = bot;
selfhostedInstance.isAdding = !bot;
}
public toggleBot(bot: FE_CustomSimpleBot) {
@ -66,11 +79,15 @@ export class AdminCustomBotsComponent {
bot.isEnabled = !bot.isEnabled;
this.botApi.updateBot(bot.id, bot).then(() => {
this.isUpdating = false;
this.translate.get(['Enabled', 'disabled']).subscribe((res: string) => {this.toaster.pop("success", "Bot " + (bot.isEnabled ? res[0] : res[1])); });
this.translate.get(['Enabled', 'disabled']).subscribe((res: string) => {
this.toaster.pop("success", "Bot " + (bot.isEnabled ? res[0] : res[1]));
});
}).catch(error => {
console.error(error);
bot.isEnabled = !bot.isEnabled;
this.translate.get('Error updating bot').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error updating bot').subscribe((res: string) => {
this.toaster.pop("error", res);
});
})
}
}

View File

@ -2,12 +2,11 @@ import { Component } from "@angular/core";
import { AdminApiService } from "../../shared/services/admin/admin-api.service";
import { FE_DimensionConfig } from "../../shared/models/admin-responses";
import { ToasterService } from "angular2-toaster";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import {
AdminLogoutConfirmationDialogComponent,
LogoutConfirmationDialogContext
} from "./logout-confirmation/logout-confirmation.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
templateUrl: "./home.component.html",
@ -19,9 +18,9 @@ export class AdminHomeComponent {
public config: FE_DimensionConfig;
constructor(private adminApi: AdminApiService,
private toaster: ToasterService,
private modal: Modal,
public translate: TranslateService) {
private toaster: ToasterService,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
adminApi.getConfig().then(config => {
this.config = config;
@ -30,16 +29,22 @@ export class AdminHomeComponent {
}
public logoutAll(): void {
this.modal.open(AdminLogoutConfirmationDialogComponent, overlayConfigFactory({
isBlocking: true,
}, LogoutConfirmationDialogContext)).result.then(() => {
this.adminApi.logoutAll().then(() => {
this.translate.get('Everyone has been logged out').subscribe((res: string) => {this.toaster.pop("success", res); });
const selfhostedRef = this.modal.open(AdminLogoutConfirmationDialogComponent, {
backdrop: 'static'
});
selfhostedRef.result.then(async () => {
try {
await this.adminApi.logoutAll();
this.translate.get('Everyone has been logged out').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.config.sessionInfo.numTokens = 0;
}).catch(err => {
} catch (err) {
console.error(err);
this.translate.get('Error logging everyone out').subscribe((res: string) => {this.toaster.pop("error", res); });
});
this.translate.get('Error logging everyone out').subscribe((res: string) => {
this.toaster.pop("error", res);
});
}
});
}
}

View File

@ -1,18 +1,17 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'Logout confirmation' | translate}}</h4>
</div>
<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.' | translate}}
</p>
</div>
<div class="dialog-footer">
<button type="button" (click)="dialog.dismiss()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="logout everyone" class="btn btn-danger btn-sm">
<i class="far fa-times-circle"></i> {{'Logout Everyone' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'Logout confirmation' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body">
<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.' | translate}}
</p>
</div>
<div class="modal-footer">
<button type="button" (click)="modal.dismiss()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
<button type="button" (click)="modal.close()" title="logout everyone" class="btn btn-danger btn-sm">
<i class="far fa-times-circle"></i> {{'Logout Everyone' | translate}}
</button>
</div>

View File

@ -1,16 +1,15 @@
import { Component } from "@angular/core";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
export class LogoutConfirmationDialogContext extends BSModalContext {
export interface LogoutConfirmationDialogContext {
}
@Component({
templateUrl: "./logout-confirmation.component.html",
styleUrls: ["./logout-confirmation.component.scss"],
})
export class AdminLogoutConfirmationDialogComponent implements ModalComponent<LogoutConfirmationDialogContext> {
export class AdminLogoutConfirmationDialogComponent {
constructor(public dialog: DialogRef<LogoutConfirmationDialogContext>) {
constructor(public modal: NgbActiveModal) {
}
}

View File

@ -3,12 +3,12 @@ import { AdminAppserviceApiService } from "../../../shared/services/admin/admin-
import { AdminNebApiService } from "../../../shared/services/admin/admin-neb-api.service";
import { ToasterService } from "angular2-toaster";
import { ActivatedRoute, Router } from "@angular/router";
import { Modal, overlayConfigFactory } from "ngx-modialog";
import {
AdminNebAppserviceConfigComponent,
AppserviceConfigDialogContext
} from "../appservice-config/appservice-config.component";
import { TranslateService } from "@ngx-translate/core";
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@Component({
@ -22,12 +22,12 @@ export class AdminAddSelfhostedNebComponent {
public adminUrl = "http://localhost:4050";
constructor(private asApi: AdminAppserviceApiService,
private nebApi: AdminNebApiService,
private toaster: ToasterService,
private router: Router,
private activatedRoute: ActivatedRoute,
private modal: Modal,
public translate: TranslateService) {
private nebApi: AdminNebApiService,
private toaster: ToasterService,
private router: Router,
private activatedRoute: ActivatedRoute,
private modal: NgbModal,
public translate: TranslateService) {
this.translate = translate;
}
@ -36,17 +36,23 @@ export class AdminAddSelfhostedNebComponent {
this.asApi.createAppservice(this.userPrefix).then(appservice => {
return this.nebApi.newAppserviceConfiguration(this.adminUrl, appservice);
}).then(neb => {
this.translate.get('New go-neb created').subscribe((res: string) => {this.toaster.pop("success", res); });
this.modal.open(AdminNebAppserviceConfigComponent, overlayConfigFactory({
neb: neb,
this.translate.get('New go-neb created').subscribe((res: string) => {
this.toaster.pop("success", res);
});
isBlocking: true,
const selfhostedRef = this.modal.open(AdminNebAppserviceConfigComponent, {
backdrop: 'static',
size: 'lg',
}, AppserviceConfigDialogContext)).result.then(() => this.router.navigate(["../.."], {relativeTo: this.activatedRoute}));
});
selfhostedRef.result.then(() => this.router.navigate(["../.."], {relativeTo: this.activatedRoute}));
const selfhostedInstance = selfhostedRef.componentInstance as AppserviceConfigDialogContext;
selfhostedInstance.neb = neb;
}).catch(err => {
console.error(err);
this.isSaving = false;
this.translate.get('Error creating appservice').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error creating appservice').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,22 +1,21 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'go-neb appservice configuration' | translate}}</h4>
</div>
<div class="dialog-content" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="dialog-content" *ngIf="!isLoading">
{{'Copy and paste this configuration to' | translate}} <code>appservice-{{appservice.id}}.yaml</code>
{{'on your homeserver and register it as an application service.' | translate}}
<br/>
<pre>{{appserviceConfig}}</pre>
</div>
<div class="dialog-footer" *ngIf="!isLoading">
<button type="button" (click)="dialog.close()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-times-circle"></i> {{'Close' | translate}}
</button>
<button type="button" (click)="test()" title="close" class="btn btn-secondary btn-sm">
<i class="fa fa-exchange-alt"></i> {{'Test Configuration' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'go-neb appservice configuration' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body" *ngif="isLoading">
<my-spinner></my-spinner>
</div>
<div class="modal-body" *ngif="!isLoading">
{{'Copy and paste this configuration to' | translate}} <code>appservice-{{appservice.id}}.yaml</code>
{{'on your homeserver and register it as an application service.' | translate}}
<br/>
<pre>{{appserviceConfig}}</pre>
</div>
<div class="modal-footer">
<button type="button" (click)="test()" title="save" class="btn btn-primary btn-sm">
<i class="fa fa-exchange-alt"></i> {{'Test Configuration' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,38 +1,37 @@
import { Component } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { FE_Appservice, FE_NebConfiguration } from "../../../shared/models/admin-responses";
import { AdminAppserviceApiService } from "../../../shared/services/admin/admin-appservice-api.service";
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { TranslateService } from "@ngx-translate/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
export class AppserviceConfigDialogContext extends BSModalContext {
public neb: FE_NebConfiguration;
export interface AppserviceConfigDialogContext {
neb: FE_NebConfiguration;
}
@Component({
templateUrl: "./appservice-config.component.html",
styleUrls: ["./appservice-config.component.scss"],
})
export class AdminNebAppserviceConfigComponent implements ModalComponent<AppserviceConfigDialogContext> {
export class AdminNebAppserviceConfigComponent {
public isLoading = true;
public neb: FE_NebConfiguration;
public appservice: FE_Appservice;
constructor(public dialog: DialogRef<AppserviceConfigDialogContext>,
private adminAppserviceApi: AdminAppserviceApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private adminAppserviceApi: AdminAppserviceApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.neb = dialog.context.neb;
this.adminAppserviceApi.getAppservice(this.neb.appserviceId).then(appservice => {
this.appservice = appservice;
this.isLoading = false;
}).catch(err => {
console.error(err);
this.translate.get('Could not load appservice configuration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Could not load appservice configuration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
@ -53,10 +52,14 @@ export class AdminNebAppserviceConfigComponent implements ModalComponent<Appserv
public test() {
this.adminAppserviceApi.test(this.neb.appserviceId).then(() => {
this.translate.get('The appservice appears to be correctly set up').subscribe((res: string) => {this.toaster.pop("success", res); });
this.translate.get('The appservice appears to be correctly set up').subscribe((res: string) => {
this.toaster.pop("success", res);
});
}).catch(err => {
console.error(err);
this.translate.get('The appservice is not correctly set up').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('The appservice is not correctly set up').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,8 +1,7 @@
import { BSModalContext } from "ngx-modialog/plugins/bootstrap";
import { FE_NebConfiguration } from "../../../shared/models/admin-responses";
import { FE_Integration } from "../../../shared/models/integration";
export class NebBotConfigurationDialogContext extends BSModalContext {
public integration: FE_Integration;
public neb: FE_NebConfiguration;
export interface NebBotConfigurationDialogContext {
integration: FE_Integration;
neb: FE_NebConfiguration;
}

View File

@ -1,33 +1,32 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'Giphy Configuration' | translate}}</h4>
</div>
<div class="dialog-content" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="dialog-content" *ngIf="!isLoading">
<label class="label-block">
{{'Api Key' | translate}}
<span class="text-muted ">{{'The API key from' | translate}} <a href="https://developers.giphy.com/" target="_blank">developers.giphy.com</a>.</span>
<input type="text" class="form-control"
placeholder="your_api_key_here"
[(ngModel)]="config.api_key" [disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Image Size' | translate}}
<span class="text-muted ">{{'GIFs can be large, and sometimes it is more desirable to have them downsized.' | translate}}</span>
<label class="checkbox">
<input type="checkbox" [(ngModel)]="config.use_downsized" [disabled]="isUpdating"/>
{{'Use downsized images' | translate}}
</label>
</label>
</div>
<div class="dialog-footer" *ngIf="!isLoading">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'Giphy Configuration' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="modal-body" *ngIf="!isLoading">
<label class="label-block">
{{'Api Key' | translate}}
<span class="text-muted ">{{'The API key from' | translate}} <a href="https://developers.giphy.com/" target="_blank">developers.giphy.com</a>.</span>
<input type="text" class="form-control"
placeholder="your_api_key_here"
[(ngModel)]="config.api_key" [disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Image Size' | translate}}
<span class="text-muted ">{{'GIFs can be large, and sometimes it is more desirable to have them downsized.' | translate}}</span>
<label class="checkbox">
<input type="checkbox" [(ngModel)]="config.use_downsized" [disabled]="isUpdating"/>
{{'Use downsized images' | translate}}
</label>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,11 +1,10 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { NebBotConfigurationDialogContext } from "../config-context";
import { AdminNebApiService } from "../../../../shared/services/admin/admin-neb-api.service";
import { FE_NebConfiguration } from "../../../../shared/models/admin-responses";
import { FE_Integration } from "../../../../shared/models/integration";
import { TranslateService } from "@ngx-translate/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
interface GiphyConfig {
api_key: string;
@ -16,7 +15,7 @@ interface GiphyConfig {
templateUrl: "./giphy.component.html",
styleUrls: ["./giphy.component.scss", "../config-dialog.scss"],
})
export class AdminNebGiphyConfigComponent implements ModalComponent<NebBotConfigurationDialogContext>, OnInit {
export class AdminNebGiphyConfigComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
@ -24,13 +23,11 @@ export class AdminNebGiphyConfigComponent implements ModalComponent<NebBotConfig
public integration: FE_Integration;
public neb: FE_NebConfiguration;
constructor(public dialog: DialogRef<NebBotConfigurationDialogContext>,
private adminNebApi: AdminNebApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private adminNebApi: AdminNebApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.neb = dialog.context.neb;
this.integration = dialog.context.integration;
}
public ngOnInit() {
@ -39,19 +36,25 @@ export class AdminNebGiphyConfigComponent implements ModalComponent<NebBotConfig
this.isLoading = false;
}).catch(err => {
console.error(err);
this.translate.get('Error loading configuration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading configuration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public save() {
this.isUpdating = true;
this.adminNebApi.setIntegrationConfiguration(this.neb.id, this.integration.type, this.config).then(() => {
this.translate.get('Configuration updated').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Configuration updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
this.isUpdating = false;
console.error(err);
this.translate.get('Error updating integration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error updating integration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,32 +1,31 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'Google Configuration' | translate}}</h4>
</div>
<div class="dialog-content" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="dialog-content" *ngIf="!isLoading">
<label class="label-block">
{{'Api Key' | translate}}
<span class="text-muted ">{{'The API key for your Google Application.' | translate}}</span>
<input type="text" class="form-control"
placeholder="your_api_key_here"
[(ngModel)]="config.api_key" [disabled]="isUpdating"/>
</label>
<label class="label-block">
Search Engine ID
<span class="text-muted ">{{'The search engine ID' | translate}}</span>
<input type="text" class="form-control"
placeholder="your_cx_id_here"
[(ngModel)]="config.cx" [disabled]="isUpdating"/>
</label>
</div>
<div class="dialog-footer" *ngIf="!isLoading">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'Google Configuration' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="modal-body" *ngIf="!isLoading">
<label class="label-block">
{{'Api Key' | translate}}
<span class="text-muted ">{{'The API key for your Google Application.' | translate}}</span>
<input type="text" class="form-control"
placeholder="your_api_key_here"
[(ngModel)]="config.api_key" [disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Search Engine ID' | translate}}
<span class="text-muted ">{{'The search engine ID' | translate}}</span>
<input type="text" class="form-control"
placeholder="your_cx_id_here"
[(ngModel)]="config.cx" [disabled]="isUpdating"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,11 +1,10 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { NebBotConfigurationDialogContext } from "../config-context";
import { AdminNebApiService } from "../../../../shared/services/admin/admin-neb-api.service";
import { FE_NebConfiguration } from "../../../../shared/models/admin-responses";
import { FE_Integration } from "../../../../shared/models/integration";
import { TranslateService } from "@ngx-translate/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
interface GoogleConfig {
api_key: string;
@ -16,7 +15,7 @@ interface GoogleConfig {
templateUrl: "./google.component.html",
styleUrls: ["./google.component.scss", "../config-dialog.scss"],
})
export class AdminNebGoogleConfigComponent implements ModalComponent<NebBotConfigurationDialogContext>, OnInit {
export class AdminNebGoogleConfigComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
@ -24,13 +23,11 @@ export class AdminNebGoogleConfigComponent implements ModalComponent<NebBotConfi
public integration: FE_Integration;
public neb: FE_NebConfiguration;
constructor(public dialog: DialogRef<NebBotConfigurationDialogContext>,
private adminNebApi: AdminNebApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private adminNebApi: AdminNebApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.neb = dialog.context.neb;
this.integration = dialog.context.integration;
}
public ngOnInit() {
@ -39,19 +36,25 @@ export class AdminNebGoogleConfigComponent implements ModalComponent<NebBotConfi
this.isLoading = false;
}).catch(err => {
console.error(err);
this.translate.get('Error loading configuration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading configuration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public save() {
this.isUpdating = true;
this.adminNebApi.setIntegrationConfiguration(this.neb.id, this.integration.type, this.config).then(() => {
this.translate.get('Configuration updated').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Configuration updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
this.isUpdating = false;
console.error(err);
this.translate.get('Error updating integration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error updating integration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,25 +1,24 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'Guggy Configuration' | translate}}</h4>
</div>
<div class="dialog-content" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="dialog-content" *ngIf="!isLoading">
<label class="label-block">
{{'Api Key' | translate}}
<span class="text-muted ">{{'The API key for' | translate}} <a href="http://docs.guggy.com/" target="_blank">Guggy's API</a>.</span>
<input type="text" class="form-control"
placeholder="your_api_key_here"
[(ngModel)]="config.api_key" [disabled]="isUpdating"/>
</label>
</div>
<div class="dialog-footer" *ngIf="!isLoading">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'Guggy Configuration' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="modal-body" *ngIf="!isLoading">
<label class="label-block">
{{'Api Key' | translate}}
<span class="text-muted ">{{'The API key for' | translate}} <a href="http://docs.guggy.com/" target="_blank">Guggy's API</a>.</span>
<input type="text" class="form-control"
placeholder="your_api_key_here"
[(ngModel)]="config.api_key" [disabled]="isUpdating"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

View File

@ -1,11 +1,10 @@
import { Component, OnInit } from "@angular/core";
import { ToasterService } from "angular2-toaster";
import { DialogRef, ModalComponent } from "ngx-modialog";
import { NebBotConfigurationDialogContext } from "../config-context";
import { AdminNebApiService } from "../../../../shared/services/admin/admin-neb-api.service";
import { FE_NebConfiguration } from "../../../../shared/models/admin-responses";
import { FE_Integration } from "../../../../shared/models/integration";
import { TranslateService } from "@ngx-translate/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
interface GuggyConfig {
api_key: string;
@ -15,7 +14,7 @@ interface GuggyConfig {
templateUrl: "./guggy.component.html",
styleUrls: ["./guggy.component.scss", "../config-dialog.scss"],
})
export class AdminNebGuggyConfigComponent implements ModalComponent<NebBotConfigurationDialogContext>, OnInit {
export class AdminNebGuggyConfigComponent implements OnInit {
public isLoading = true;
public isUpdating = false;
@ -23,13 +22,11 @@ export class AdminNebGuggyConfigComponent implements ModalComponent<NebBotConfig
public integration: FE_Integration;
public neb: FE_NebConfiguration;
constructor(public dialog: DialogRef<NebBotConfigurationDialogContext>,
private adminNebApi: AdminNebApiService,
private toaster: ToasterService,
public translate: TranslateService) {
constructor(public modal: NgbActiveModal,
private adminNebApi: AdminNebApiService,
private toaster: ToasterService,
public translate: TranslateService) {
this.translate = translate;
this.neb = dialog.context.neb;
this.integration = dialog.context.integration;
}
public ngOnInit() {
@ -38,19 +35,25 @@ export class AdminNebGuggyConfigComponent implements ModalComponent<NebBotConfig
this.isLoading = false;
}).catch(err => {
console.error(err);
this.translate.get('Error loading configuration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error loading configuration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
public save() {
this.isUpdating = true;
this.adminNebApi.setIntegrationConfiguration(this.neb.id, this.integration.type, this.config).then(() => {
this.translate.get('Configuration updated').subscribe((res: string) => {this.toaster.pop("success", res); });
this.dialog.close();
this.translate.get('Configuration updated').subscribe((res: string) => {
this.toaster.pop("success", res);
});
this.modal.close();
}).catch(err => {
this.isUpdating = false;
console.error(err);
this.translate.get('Error updating integration').subscribe((res: string) => {this.toaster.pop("error", res); });
this.translate.get('Error updating integration').subscribe((res: string) => {
this.toaster.pop("error", res);
});
});
}
}

View File

@ -1,32 +1,31 @@
<div class="dialog">
<div class="dialog-header">
<h4>{{'Imgur Configuration' | translate}}</h4>
</div>
<div class="dialog-content" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="dialog-content" *ngIf="!isLoading">
<label class="label-block">
{{'Client ID' | translate}}
<span class="text-muted ">{{'The client ID of your' | translate}} <a href="https://apidocs.imgur.com/" target="_blank">{{'Imgur Application' | translate}}</a>.</span>
<input type="text" class="form-control"
placeholder="your_client_id"
[(ngModel)]="config.client_id" [disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Client Secret' | translate}}
<span class="text-muted ">{{'The client secret of your' | translate}} <a href="https://apidocs.imgur.com/" target="_blank">{{'Imgur Application' | translate}}</a>.</span>
<input type="text" class="form-control"
placeholder="your_client_secret"
[(ngModel)]="config.client_secret" [disabled]="isUpdating"/>
</label>
</div>
<div class="dialog-footer" *ngIf="!isLoading">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="dialog.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{'Imgur Configuration' | translate}}</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" (click)="modal.close()"></button>
</div>
<div class="modal-body" *ngIf="isLoading">
<my-spinner></my-spinner>
</div>
<div class="modal-body" *ngIf="!isLoading">
<label class="label-block">
{{'Client ID' | translate}}
<span class="text-muted ">{{'The client ID of your' | translate}} <a href="https://apidocs.imgur.com/" target="_blank">{{'Imgur Application' | translate}}</a>.</span>
<input type="text" class="form-control"
placeholder="your_client_id"
[(ngModel)]="config.client_id" [disabled]="isUpdating"/>
</label>
<label class="label-block">
{{'Client Secret' | translate}}
<span class="text-muted ">{{'The client secret of your' | translate}} <a href="https://apidocs.imgur.com/" target="_blank">{{'Imgur Application' | translate}}</a>.</span>
<input type="text" class="form-control"
placeholder="your_client_secret"
[(ngModel)]="config.client_secret" [disabled]="isUpdating"/>
</label>
</div>
<div class="modal-footer">
<button type="button" (click)="save()" title="save" class="btn btn-primary btn-sm">
<i class="far fa-save"></i> {{'Save' | translate}}
</button>
<button type="button" (click)="modal.close()" title="close" class="btn btn-secondary btn-sm">
<i class="far fa-times-circle"></i> {{'Cancel' | translate}}
</button>
</div>

Some files were not shown because too many files have changed in this diff Show More