├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── feature.md │ ├── task.md │ ├── task-event.md │ ├── test.md │ ├── use-case.md │ ├── enhancement.md │ └── bug_report.md ├── babel.config.json ├── scripts ├── pull-all.sh ├── push-all.sh ├── set-branch-main.sh ├── configure-all.sh ├── add-hg-module.sh └── branch-all.sh ├── src ├── hghs.ts ├── constants │ ├── runtime.ts │ └── build.ts ├── main.ts └── controllers │ └── HsBackendController.ts ├── .gitmodules ├── docker-compose.yml ├── tsconfig.json ├── Dockerfile ├── LICENSE ├── package.json ├── .gitignore ├── README.md └── rollup.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [heusalagroup] 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "8" 6 | } 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/pull-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")/.." 3 | set -x 4 | git pull 5 | cat .gitmodules |grep -F path|awk '{print $3}'|while read DIR; do 6 | (cd $DIR && git pull)& 7 | done|cat 8 | -------------------------------------------------------------------------------- /scripts/push-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")/.." 3 | set -x 4 | git push 5 | cat .gitmodules |grep -F path|awk '{print $3}'|while read DIR; do 6 | (cd $DIR && git push)& 7 | done|cat 8 | -------------------------------------------------------------------------------- /scripts/set-branch-main.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")/.." 3 | set -x 4 | #git checkout main 5 | cat .gitmodules |grep -F path|awk '{print $3}'|while read DIR; do 6 | (cd $DIR && git checkout main); 7 | done 8 | -------------------------------------------------------------------------------- /scripts/configure-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")/.." 3 | set -x 4 | git config pull.rebase false 5 | cat .gitmodules |grep -F path|awk '{print $3}'|while read DIR; do 6 | (cd $DIR && git config pull.rebase false); 7 | done 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: New feature 4 | title: "[FT] [TBD HTP] Subject" 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | [Index: TBD] 10 | 11 | ## Related tests, tasks & bugs 12 | 13 | * #N 14 | 15 | ## Description 16 | 17 | Description of the feature 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: New work item for feature 4 | title: "[TASK] [TBD HTP] Subject" 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | [Feature: TBD] 10 | 11 | ## Related tests, tasks & bugs 12 | 13 | * #N 14 | 15 | ## Description 16 | 17 | Description of the work item 18 | -------------------------------------------------------------------------------- /src/hghs.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023. Heusala Group . All rights reserved. 2 | 3 | import { main } from "./main"; 4 | main(process.argv).then((status : number) => { 5 | process.exit(status); 6 | }).catch((err : any) => { 7 | console.error(`Error: `, err); 8 | process.exit(1); 9 | }); 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task-event.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Event task 3 | about: New event task 4 | title: "[TASK] [TBD HTP] Event m.something" 5 | labels: task 6 | assignees: '' 7 | 8 | --- 9 | [Feature: TBD] 10 | 11 | * Event Name: `m.something` 12 | * Constant name: `MatrixType.M_SOMETHING` 13 | 14 | ## Related tests, tasks & bugs 15 | 16 | * #N 17 | 18 | ## Description 19 | 20 | Description of the feature 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | about: Create a new test case 4 | title: "[TEST] Subject" 5 | labels: testcase 6 | assignees: '' 7 | 8 | --- 9 | [Feature: TBD] 10 | 11 | ## Initial state 12 | 13 | * [ ] User has no session on the server 14 | 15 | ## Steps 16 | 17 | 1. [ ] Step one 18 | 19 | ## End results 20 | 21 | * [ ] UI shows a thank you page 22 | 23 | ## Bugs related, if failed 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/use-case.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Use case 3 | about: Create a new use case 4 | title: "[UC] Subject" 5 | labels: usecase 6 | assignees: '' 7 | 8 | --- 9 | [Feature: TBD] 10 | 11 | ## Initial state 12 | 13 | * [ ] User has no session on the server 14 | 15 | ## Steps 16 | 17 | 1. [ ] Step one 18 | 19 | ## End results 20 | 21 | * [ ] UI shows a thank you page 22 | 23 | ## Tests related, if any 24 | 25 | ## Bugs related, if any 26 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/fi/hg/core"] 2 | url = git@github.com:heusalagroup/fi.hg.core.git 3 | path = src/fi/hg/core 4 | branch = main 5 | [submodule "src/fi/hg/matrix"] 6 | path = src/fi/hg/matrix 7 | url = git@github.com:heusalagroup/fi.hg.matrix.git 8 | branch = main 9 | [submodule "src/fi/hg/backend"] 10 | path = src/fi/hg/backend 11 | url = git@github.com:heusalagroup/fi.hg.backend.git 12 | branch = main 13 | [submodule "src/fi/hg/node"] 14 | path = src/fi/hg/node 15 | url = git@github.com:heusalagroup/fi.hg.node.git 16 | branch = main 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | 5 | hghs-backend: 6 | image: "hghs-backend" 7 | container_name: "hghs-backend" 8 | hostname: "hghs-backend" 9 | restart: "unless-stopped" 10 | build: . 11 | environment: 12 | PORT: "8008" 13 | BACKEND_LOG_LEVEL: "DEBUG" 14 | BACKEND_URL: "http://0.0.0.0:8008" 15 | BACKEND_PUBLIC_URL: "http://localhost:8008" 16 | BACKEND_INITIAL_USERS: "$BACKEND_INITIAL_USERS" 17 | BACKEND_EMAIL_CONFIG: "smtp://hghs-test-smtp:1025" 18 | BACKEND_JWT_SECRET: "$BACKEND_JWT_SECRET" 19 | ports: 20 | - "8008:8008" 21 | - "8443:8443" 22 | -------------------------------------------------------------------------------- /scripts/add-hg-module.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")/.." 3 | set -e 4 | #set -x 5 | 6 | NAME="$1" 7 | 8 | if test "x$NAME" = x; then 9 | echo 'USAGE: ./scripts/add-hg-module.sh NAME' >&2 10 | exit 1 11 | fi 12 | 13 | ORG='heusalagroup' 14 | BRANCH='main' 15 | DIR="src/fi/hg/$NAME" 16 | REPO_NAME="$ORG/fi.hg.$NAME" 17 | REPO_URL="git@github.com:$REPO_NAME.git" 18 | 19 | if test -d "$DIR"; then 20 | git config -f .gitmodules "submodule.$DIR.path" "$DIR" 21 | git config -f .gitmodules "submodule.$DIR.url" "$REPO_URL" 22 | else 23 | git submodule add "$REPO_URL" "$DIR" 24 | fi 25 | git config -f .gitmodules "submodule.$DIR.branch" "$BRANCH" 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: Subject 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | [Feature: TBD] 10 | 11 | ## Is your feature request related to a problem? Please describe. 12 | 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | ## Describe the solution you'd like 16 | 17 | A clear and concise description of what you want to happen. 18 | 19 | ## Describe alternatives you've considered 20 | 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | ## Additional context 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /scripts/branch-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")/.." 3 | #set -x 4 | 5 | FORMAT='%-15s %-22s %-20s %s\n' 6 | 7 | ROOT_BRANCH="$( 8 | git branch \ 9 | | grep -E '^\*' \ 10 | | sed -re 's/^\* *//' \ 11 | | tr -d '\n' 12 | )" 13 | 14 | printf "$FORMAT" "$ROOT_BRANCH" "." "." "." "." 15 | 16 | cat .gitmodules |grep -F path|awk '{print $3}'|while read DIR; do 17 | ( 18 | cd $DIR 19 | MODULE=$(echo $DIR|sed -re 's@^.*/src/@@'|tr '/' '.') 20 | TAG=$((git describe --tags 2>/dev/null)|sort -n|tail -n1) 21 | if test "x$TAG" = x; then 22 | TAG="$(git rev-parse --short HEAD)" 23 | fi 24 | BRANCH="$( 25 | git branch \ 26 | | grep -E '^\*' \ 27 | | sed -re 's/^\* *//' \ 28 | | tr -d '\n' 29 | )" 30 | 31 | printf "$FORMAT" "$BRANCH" "$MODULE" "$TAG" "$DIR" 32 | ); 33 | done 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "esnext", 6 | "dom" 7 | ], 8 | "allowJs": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | //"declaration": true, 12 | //"declarationDir": "./dist", 13 | "moduleResolution": "node", 14 | "module": "esnext", 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "sourceMap": false, 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true, 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true, 22 | "strictNullChecks": false, 23 | "skipLibCheck": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "typeRoots": [ 26 | "./node_modules/@types/" 27 | ], 28 | "noEmit": true 29 | 30 | }, 31 | "include": [ 32 | "./src/hghs.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_IMAGE=node:20 2 | 3 | FROM $NODE_IMAGE as node-image 4 | 5 | FROM node-image 6 | 7 | ARG DEFAULT_NODE_ENV=production 8 | ARG DEFAULT_GENERATE_SOURCEMAP=false 9 | ARG DEFAULT_PUBLIC_URL=http://localhost:8008 10 | 11 | ENV PATH=/app/node_modules/.bin:$PATH 12 | ENV REACT_APP_PUBLIC_URL=$DEFAULT_PUBLIC_URL 13 | ENV NODE_ENV=$DEFAULT_NODE_ENV 14 | ENV GENERATE_SOURCEMAP=$DEFAULT_GENERATE_SOURCEMAP 15 | ENV BACKEND_LOG_LEVEL=DEBUG 16 | ENV BACKEND_URL='http://0.0.0.0:8008' 17 | ENV BACKEND_PUBLIC_URL='http://localhost:8008' 18 | ENV BACKEND_INITIAL_USERS='' 19 | ENV BACKEND_JWT_SECRET='' 20 | 21 | EXPOSE 8008 22 | EXPOSE 8448 23 | 24 | WORKDIR /app 25 | COPY ./package*.json ./ 26 | RUN [ "npm", "ci", "--silent", "--also=dev" ] 27 | COPY tsconfig.json ./tsconfig.json 28 | COPY rollup.config.js ./rollup.config.js 29 | COPY babel.config.json ./babel.config.json 30 | COPY src ./src 31 | RUN [ "npm", "run", "build" ] 32 | 33 | CMD [ "npm", "-s", "run", "start-prod" ] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Heusala Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Subject" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | [Feature: TBD] 10 | 11 | ## Related tests, tasks & bugs 12 | 13 | * #N 14 | 15 | ## Describe the bug 16 | 17 | A clear and concise description of what the bug is. 18 | 19 | ## To Reproduce 20 | 21 | Steps to reproduce the behavior: 22 | 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | ## Expected behavior 29 | 30 | A clear and concise description of what you expected to happen. 31 | 32 | ## Screenshots 33 | 34 | If applicable, add screenshots to help explain your problem. 35 | 36 | ## Desktop (please complete the following information): 37 | 38 | - OS: [e.g. iOS] 39 | - Browser: [e.g. chrome, safari] 40 | - Version: [e.g. 22] 41 | 42 | ## Smartphone (please complete the following information): 43 | 44 | - Device: [e.g. iPhone6] 45 | - OS: [e.g. iOS8.1] 46 | - Browser: [e.g. stock browser, safari] 47 | - Version: [e.g. 22] 48 | 49 | ## Additional context 50 | 51 | Add any other context about the problem here, like links to related test cases, etc. 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hghs", 3 | "version": "0.0.1", 4 | "description": "HG HomeServer", 5 | "main": "dist/hghs.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start-prod": "node dist/hghs.js", 9 | "start": "ts-node src/hghs.ts", 10 | "build": "rollup -c" 11 | }, 12 | "author": "Jaakko Heusala ", 13 | "license": "MIT", 14 | "private": true, 15 | "bin": { 16 | "hghs": "dist/hghs.js" 17 | }, 18 | "keywords": [ 19 | "typescript", 20 | "backend", 21 | "rest", 22 | "nodejs", 23 | "spring", 24 | "spring-boot" 25 | ], 26 | "devDependencies": { 27 | "@babel/cli": "^7.22.10", 28 | "@babel/core": "^7.22.10", 29 | "@babel/preset-env": "^7.22.10", 30 | "@babel/preset-typescript": "^7.22.5", 31 | "@rollup/plugin-babel": "^6.0.3", 32 | "@rollup/plugin-commonjs": "^25.0.3", 33 | "@rollup/plugin-inject": "^5.0.3", 34 | "@rollup/plugin-json": "^6.0.0", 35 | "@rollup/plugin-node-resolve": "^15.1.0", 36 | "@rollup/plugin-replace": "^5.0.2", 37 | "@rollup/plugin-typescript": "^11.1.2", 38 | "@rollup/plugin-url": "^8.0.1", 39 | "@types/jws": "^3.2.5", 40 | "@types/lodash": "^4.14.197", 41 | "@types/node": "^20.4.10", 42 | "@types/nodemailer": "^6.4.9", 43 | "i18next": "^23.4.4", 44 | "jws": "^4.0.0", 45 | "lodash": "^4.17.21", 46 | "nodemailer": "^6.9.4", 47 | "rollup": "^3.26.3", 48 | "rollup-plugin-uglify": "github:heusalagroup/rollup-plugin-uglify", 49 | "ts-node": "^10.9.1", 50 | "tslib": "^2.6.1", 51 | "typescript": "^5.1.6", 52 | "reflect-metadata": "^0.1.13" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /src/constants/runtime.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023. Heusala Group . All rights reserved. 2 | 3 | import { parseNonEmptyString } from "../fi/hg/core/types/String"; 4 | import { LogLevel, parseLogLevel } from "../fi/hg/core/types/LogLevel"; 5 | import { 6 | BUILD_COMMAND_NAME, 7 | BUILD_LOG_LEVEL, 8 | BUILD_BACKEND_URL, 9 | BUILD_BACKEND_HOSTNAME, 10 | BUILD_JWT_SECRET, 11 | BUILD_JWT_ALG, 12 | BUILD_DEFAULT_LANGUAGE, 13 | BUILD_EMAIL_CONFIG, 14 | BUILD_EMAIL_FROM, 15 | BUILD_ACCESS_TOKEN_EXPIRATION_TIME, 16 | BUILD_BACKEND_PUBLIC_URL, 17 | BUILD_FEDERATION_URL 18 | } from "./build"; 19 | 20 | export const BACKEND_LOG_LEVEL : LogLevel = parseLogLevel(parseNonEmptyString(process?.env?.BACKEND_LOG_LEVEL) ?? parseNonEmptyString(BUILD_LOG_LEVEL)) ?? LogLevel.INFO ; 21 | export const BACKEND_SCRIPT_NAME : string = parseNonEmptyString(process?.env?.BACKEND_SCRIPT_NAME) ?? BUILD_COMMAND_NAME; 22 | export const BACKEND_URL : string = parseNonEmptyString(process?.env?.BACKEND_URL) ?? BUILD_BACKEND_URL; 23 | export const FEDERATION_URL : string = parseNonEmptyString(process?.env?.FEDERATION_URL) ?? BUILD_FEDERATION_URL; 24 | export const BACKEND_PUBLIC_URL : string = parseNonEmptyString(process?.env?.BACKEND_PUBLIC_URL) ?? BUILD_BACKEND_PUBLIC_URL; 25 | export const BACKEND_HOSTNAME : string = parseNonEmptyString(process?.env?.BACKEND_HOSTNAME) ?? BUILD_BACKEND_HOSTNAME; 26 | export const BACKEND_JWT_SECRET : string = parseNonEmptyString(process?.env?.BACKEND_JWT_SECRET) ?? BUILD_JWT_SECRET; 27 | export const BACKEND_JWT_ALG : string = parseNonEmptyString(process?.env?.BACKEND_JWT_ALG) ?? BUILD_JWT_ALG; 28 | export const BACKEND_DEFAULT_LANGUAGE : string = parseNonEmptyString(process?.env?.BACKEND_DEFAULT_LANGUAGE) ?? BUILD_DEFAULT_LANGUAGE; 29 | export const BACKEND_EMAIL_CONFIG : string = parseNonEmptyString(process?.env?.BACKEND_EMAIL_CONFIG) ?? BUILD_EMAIL_CONFIG; 30 | export const BACKEND_EMAIL_FROM : string = parseNonEmptyString(process?.env?.BACKEND_EMAIL_FROM) ?? BUILD_EMAIL_FROM; 31 | export const BACKEND_INITIAL_USERS : string | undefined = parseNonEmptyString(process?.env?.BACKEND_INITIAL_USERS); 32 | 33 | /** 34 | * Expiration time in minutes 35 | */ 36 | export const BACKEND_ACCESS_TOKEN_EXPIRATION_TIME : string = parseNonEmptyString(process?.env?.BACKEND_ACCESS_TOKEN_EXPIRATION_TIME) ?? BUILD_ACCESS_TOKEN_EXPIRATION_TIME; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @heusalagroup/hghs 2 | 3 | *HG HomeServer* ***will be*** a zero dep [Matrix.org](https://matrix.org) HomeServer 4 | written in pure TypeScript. 5 | 6 | It's intended for special use cases when Matrix protocol is used as a backbone 7 | for custom apps. For example, we use our 8 | [MatrixCrudRepository](https://github.com/heusalagroup/fi.hg.matrix/blob/main/MatrixCrudRepository.ts) 9 | as a persistent data store for our software. It's lightweight, minimal and for the moment isn't even planned to 10 | support full Matrix spec. We might make `hghs` run on browser later; the client already does. 11 | 12 | It compiles as a single standalone JavaScript file. The only runtime dependency 13 | is NodeJS. 14 | 15 | Our software is designed for scalable and fully managed serverless cloud 16 | environments, e.g. where the software must spin up fast, can run concurrently, 17 | and can be deployed without a downtime. 18 | 19 | Another intended use case for `hghs` is embedded devices (e.g. OpenWRT), for 20 | example. 21 | 22 | It will only support [a subset of Matrix.org protocol](https://github.com/heusalagroup/hghs/issues/16) 23 | that our software is using. However, we're happy to add more features for 24 | commercial clients. 25 | 26 | ### Test driven development 27 | 28 | See [@heusalagroup/hshs-test](https://github.com/heusalagroup/hghs-test) for our 29 | system tests. 30 | 31 | ### Fetching source code 32 | 33 | #### Fetching source code using SSH 34 | 35 | ```shell 36 | git clone git@github.com:heusalagroup/hghs.git hghs 37 | cd hghs 38 | git submodule update --init --recursive 39 | ``` 40 | 41 | #### Fetching source code using HTTP 42 | 43 | Our code leans heavily on git submodules configured over ssh URLs. For http 44 | access, you'll need to set up an override to use https instead: 45 | 46 | ```shell 47 | git config --global url.https://github.com/heusalagroup/.insteadOf git@github.com:heusalagroup/ 48 | ``` 49 | 50 | This will translate any use of `git@github.com:heusalagroup/REPO` to 51 | `https://github.com/heusalagroup/REPO`. 52 | 53 | This setting can be removed using: 54 | 55 | ```shell 56 | git config --unset --global url.https://github.com/heusalagroup/.insteadOf 57 | ``` 58 | 59 | ### Build docker containers 60 | 61 | This is the easiest way to use the backend. 62 | 63 | ``` 64 | docker-compose build 65 | ``` 66 | 67 | ### Start Docker environment 68 | 69 | ``` 70 | export BACKEND_JWT_SECRET='secretJwtString123' 71 | export BACKEND_INITIAL_USERS='app:p4sSw0rd123' 72 | docker-compose up 73 | ``` 74 | 75 | Once running, services will be available: 76 | 77 | * http://localhost:8008 -- `hghs` Matrix.org Server 78 | 79 | ### Start the server in development mode 80 | 81 | FIXME: This isn't working right now. Use production mode. 82 | 83 | ``` 84 | npm start 85 | ``` 86 | 87 | ### Build the server 88 | 89 | ``` 90 | npm run build 91 | ``` 92 | 93 | ### Start the server in production mode 94 | 95 | ``` 96 | npm run start-prod 97 | ``` 98 | 99 | ...and use `http://0.0.0.0:8008` 100 | -------------------------------------------------------------------------------- /src/constants/build.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022. Heusala Group . All rights reserved. 2 | // 3 | // See also rollup.config.js 4 | // 5 | 6 | import { 7 | parseBoolean as _parseBoolean, 8 | } from "../fi/hg/core/types/Boolean"; 9 | 10 | import { 11 | parseNonEmptyString as _parseNonEmptyString 12 | } from "../fi/hg/core/types/String"; 13 | 14 | function parseBoolean (value : any) : boolean | undefined { 15 | if (value.startsWith('%'+'{') && value.endsWith('}')) return undefined; 16 | return _parseBoolean(value); 17 | } 18 | 19 | function parseNonEmptyString (value : any) : string | undefined { 20 | if (value.startsWith('%'+'{') && value.endsWith('}')) return undefined; 21 | return _parseNonEmptyString(value); 22 | } 23 | 24 | /** 25 | * @__PURE__ 26 | */ 27 | export const BUILD_USAGE_URL = 'https://github.com/heusalagroup'; 28 | 29 | /** 30 | * @__PURE__ 31 | */ 32 | export const BUILD_VERSION : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_VERSION}') ?? '?'; 33 | 34 | /** 35 | * @__PURE__ 36 | */ 37 | export const BUILD_BACKEND_URL : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_BACKEND_URL}') ?? `http://0.0.0.0:8008`; 38 | 39 | /** 40 | * @__PURE__ 41 | */ 42 | export const BUILD_FEDERATION_URL : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_FEDERATION_URL}') ?? `http://0.0.0.0:8443`; 43 | 44 | /** 45 | * @__PURE__ 46 | */ 47 | export const BUILD_BACKEND_HOSTNAME : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_BACKEND_HOSTNAME}') ?? 'localhost'; 48 | 49 | /** 50 | * @__PURE__ 51 | */ 52 | export const BUILD_JWT_SECRET : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_JWT_SECRET}') ?? ''; 53 | 54 | /** 55 | * @__PURE__ 56 | */ 57 | export const BUILD_JWT_ALG : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_JWT_ALG}') ?? 'HS256'; 58 | 59 | /** 60 | * @__PURE__ 61 | */ 62 | export const BUILD_DEFAULT_LANGUAGE : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_DEFAULT_LANGUAGE}') ?? 'en'; 63 | 64 | /** 65 | * @__PURE__ 66 | */ 67 | export const BUILD_COMMAND_NAME : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_COMMAND_NAME}') ?? 'nor-backend'; 68 | 69 | /** 70 | * @__PURE__ 71 | */ 72 | export const BUILD_LOG_LEVEL : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_LOG_LEVEL}') ?? ''; 73 | 74 | /** 75 | * @__PURE__ 76 | */ 77 | export const BUILD_NODE_ENV : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_NODE_ENV}') ?? 'development'; 78 | 79 | /** 80 | * @__PURE__ 81 | */ 82 | export const BUILD_DATE : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_DATE}') ?? ''; 83 | 84 | /** 85 | * @__PURE__ 86 | */ 87 | export const BUILD_WITH_FULL_USAGE : boolean = /* @__PURE__ */parseBoolean('%{BUILD_WITH_FULL_USAGE}') ?? true; 88 | 89 | /** 90 | * @__PURE__ 91 | */ 92 | export const IS_PRODUCTION : boolean = BUILD_NODE_ENV === 'production'; 93 | 94 | /** 95 | * @__PURE__ 96 | */ 97 | export const IS_TEST : boolean = BUILD_NODE_ENV === 'test'; 98 | 99 | /** 100 | * @__PURE__ 101 | */ 102 | export const IS_DEVELOPMENT : boolean = !IS_PRODUCTION && !IS_TEST; 103 | 104 | /** 105 | * @__PURE__ 106 | */ 107 | export const BUILD_EMAIL_FROM : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_EMAIL_FROM}') ?? 'Procure Node '; 108 | 109 | /** 110 | * @__PURE__ 111 | */ 112 | export const BUILD_EMAIL_CONFIG : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_EMAIL_CONFIG}') ?? 'smtp://localhost:25'; 113 | 114 | /** 115 | * Minutes 116 | * @__PURE__ 117 | */ 118 | export const BUILD_ACCESS_TOKEN_EXPIRATION_TIME : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_ACCESS_TOKEN_EXPIRATION_TIME}') ?? '3600'; 119 | 120 | /** 121 | * @__PURE__ 122 | */ 123 | export const BUILD_BACKEND_PUBLIC_URL : string = /* @__PURE__ */parseNonEmptyString('%{BUILD_BACKEND_PUBLIC_URL}') ?? 'http://localhost:3000'; 124 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021-2023. Heusala Group Oy . All rights reserved. 2 | 3 | const pkg = require('./package.json'); 4 | const resolve = require('@rollup/plugin-node-resolve'); 5 | const commonjs = require('@rollup/plugin-commonjs'); 6 | const typescript = require('@rollup/plugin-typescript'); 7 | const json = require('@rollup/plugin-json'); 8 | const { babel, getBabelOutputPlugin } = require('@rollup/plugin-babel'); 9 | const { uglify } = require("rollup-plugin-uglify"); 10 | const PATH = require('path'); 11 | const replace = require('@rollup/plugin-replace'); 12 | // const externalGlobals = require("rollup-plugin-external-globals"); 13 | // const inject = require('@rollup/plugin-inject'); 14 | 15 | const BUILD_VERSION = process?.env?.BUILD_VERSION ?? pkg?.version ?? ''; 16 | const BUILD_NODE_ENV = process?.env?.BUILD_NODE_ENV ?? process?.env?.NODE_ENV ?? 'production'; 17 | const BUILD_DATE = new Date().toISOString() ?? ''; 18 | const BUILD_WITH_FULL_USAGE = process?.env?.BUILD_WITH_FULL_USAGE ?? ''; 19 | const BUILD_COMMAND_NAME = process?.env?.BUILD_COMMAND_NAME ?? ''; 20 | const BUILD_LOG_LEVEL = process?.env?.BUILD_LOG_LEVEL ?? ''; 21 | 22 | console.log(`Building with options: 23 | 24 | BUILD_VERSION = '${BUILD_VERSION}' 25 | BUILD_NODE_ENV = '${BUILD_NODE_ENV}' 26 | BUILD_DATE = '${BUILD_DATE}' 27 | BUILD_COMMAND_NAME = '${BUILD_COMMAND_NAME}' 28 | BUILD_LOG_LEVEL = '${BUILD_LOG_LEVEL}' 29 | BUILD_WITH_FULL_USAGE = '${BUILD_WITH_FULL_USAGE}'`); 30 | 31 | module.exports = { 32 | input: 'src/hghs.ts', 33 | external: [ 34 | 'node:buffer', 35 | 'node:path', 36 | 'node:child_process', 37 | 'node:process', 38 | 'node:os', 39 | 'node:url' 40 | ], 41 | plugins: [ 42 | 43 | // See also ./src/runtime-constants.ts 44 | replace({ 45 | exclude: 'node_modules/**', 46 | // include: './src/build-constants.ts', 47 | values: { 48 | 'BUILD_VERSION' : BUILD_VERSION, 49 | 'BUILD_NODE_ENV' : BUILD_NODE_ENV, 50 | 'BUILD_DATE' : BUILD_DATE, 51 | 'BUILD_COMMAND_NAME' : BUILD_COMMAND_NAME, 52 | 'BUILD_LOG_LEVEL' : BUILD_LOG_LEVEL, 53 | 'BUILD_WITH_FULL_USAGE' : BUILD_WITH_FULL_USAGE 54 | }, 55 | preventAssignment: true, 56 | delimiters: ['%{', '}'] 57 | }), 58 | 59 | typescript(), 60 | 61 | json(), 62 | 63 | resolve(), 64 | 65 | commonjs({ 66 | sourceMap: false, 67 | include: /node_modules/ 68 | }), 69 | 70 | babel({ babelHelpers: 'bundled' }), 71 | 72 | // externalGlobals({ 73 | // intl: 'IntlPolyfill' 74 | // }), 75 | 76 | // See also https://github.com/mishoo/UglifyJS/blob/master/README.md#minify-options 77 | uglify({ 78 | annotations: true, 79 | toplevel: true, 80 | sourcemap: false, 81 | compress: { 82 | collapse_vars: true, 83 | imports: true, 84 | booleans: true, 85 | annotations: true, 86 | unused: true, 87 | dead_code: true, 88 | passes: 10, 89 | hoist_funs: true, 90 | hoist_vars: true, 91 | merge_vars: true, 92 | toplevel: true, 93 | unsafe_math: true 94 | }, 95 | output: { 96 | annotations: false, 97 | shebang: true, 98 | max_line_len: 120, 99 | indent_level: 2 100 | } 101 | }) 102 | 103 | ], 104 | output: { 105 | dir: 'dist', 106 | format: 'cjs', 107 | banner: '#!/usr/bin/env node', 108 | plugins: [ 109 | getBabelOutputPlugin({ 110 | configFile: PATH.resolve(__dirname, 'babel.config.json') 111 | }) 112 | ] 113 | // globals: { 114 | // intl: 'Intl' 115 | // } 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023. Heusala Group . All rights reserved. 2 | 3 | import { BackendTranslationServiceImpl } from "./fi/hg/backend/BackendTranslationServiceImpl"; 4 | import { EmailServiceImpl } from "./fi/hg/backend/EmailServiceImpl"; 5 | import { JwtEncodeServiceImpl } from "./fi/hg/backend/JwtEncodeServiceImpl"; 6 | import { EmailService } from "./fi/hg/core/email/EmailService"; 7 | import { JwtEncodeService } from "./fi/hg/core/jwt/JwtEncodeService"; 8 | import { JwtEngine } from "./fi/hg/core/jwt/JwtEngine"; 9 | import { ProcessUtils } from "./fi/hg/core/ProcessUtils"; 10 | 11 | // Must be first import to define environment variables before anything else 12 | ProcessUtils.initEnvFromDefaultFiles(); 13 | 14 | import { 15 | BACKEND_SCRIPT_NAME, 16 | BACKEND_LOG_LEVEL, 17 | BACKEND_URL, 18 | BACKEND_HOSTNAME, 19 | BACKEND_JWT_SECRET, 20 | BACKEND_JWT_ALG, 21 | BACKEND_DEFAULT_LANGUAGE, 22 | BACKEND_EMAIL_FROM, 23 | BACKEND_EMAIL_CONFIG, 24 | BACKEND_ACCESS_TOKEN_EXPIRATION_TIME, 25 | BACKEND_INITIAL_USERS, BACKEND_PUBLIC_URL, FEDERATION_URL 26 | } from "./constants/runtime"; 27 | import { 28 | BUILD_USAGE_URL, 29 | BUILD_WITH_FULL_USAGE 30 | } from "./constants/build"; 31 | 32 | import { LogService } from "./fi/hg/core/LogService"; 33 | import { RequestClientImpl } from "./fi/hg/core/RequestClientImpl"; 34 | import { RequestServer } from "./fi/hg/core/RequestServer"; 35 | import { RequestRouterImpl } from "./fi/hg/core/requestServer/RequestRouterImpl"; 36 | import { LogLevel } from "./fi/hg/core/types/LogLevel"; 37 | 38 | LogService.setLogLevel(BACKEND_LOG_LEVEL); 39 | 40 | import { TRANSLATIONS } from "./fi/hg/core/translations"; 41 | 42 | import { Headers } from "./fi/hg/core/request/types/Headers"; 43 | Headers.setLogLevel(LogLevel.INFO); 44 | 45 | import { CommandExitStatus } from "./fi/hg/core/cmd/types/CommandExitStatus"; 46 | import { CommandArgumentUtils } from "./fi/hg/core/cmd/utils/CommandArgumentUtils"; 47 | import { ParsedCommandArgumentStatus } from "./fi/hg/core/cmd/types/ParsedCommandArgumentStatus"; 48 | import { Language, parseLanguage } from "./fi/hg/core/types/Language"; 49 | import { StaticRoutes } from "./fi/hg/core/requestServer/types/StaticRoutes"; 50 | import { parseInteger } from "./fi/hg/core/types/Number"; 51 | import { MatrixServerService } from "./fi/hg/matrix/server/MatrixServerService"; 52 | import { HsBackendController } from "./controllers/HsBackendController"; 53 | import { ServerServiceImpl } from "./fi/hg/node/requestServer/ServerServiceImpl"; 54 | import { RequestServerImpl } from "./fi/hg/node/RequestServerImpl"; 55 | 56 | const LOG = LogService.createLogger('main'); 57 | 58 | export async function main ( 59 | args: string[] = [] 60 | ) : Promise { 61 | 62 | try { 63 | 64 | RequestRouterImpl.setLogLevel(LogLevel.INFO); 65 | RequestClientImpl.setLogLevel(LogLevel.INFO); 66 | // RequestServer.setLogLevel(LogLevel.INFO); 67 | StaticRoutes.setLogLevel(LogLevel.INFO); 68 | HsBackendController.setLogLevel(LogLevel.DEBUG); 69 | MatrixServerService.setLogLevel(LogLevel.DEBUG); 70 | 71 | LOG.debug(`Loglevel as ${LogService.getLogLevelString()}`); 72 | 73 | const {scriptName, parseStatus, exitStatus, errorString} = CommandArgumentUtils.parseArguments(BACKEND_SCRIPT_NAME, args); 74 | 75 | if ( parseStatus === ParsedCommandArgumentStatus.HELP || parseStatus === ParsedCommandArgumentStatus.VERSION ) { 76 | console.log(getMainUsage(scriptName)); 77 | return exitStatus; 78 | } 79 | 80 | if (errorString) { 81 | console.error(`ERROR: ${errorString}`); 82 | return exitStatus; 83 | } 84 | 85 | const jwtService : JwtEncodeService = JwtEncodeServiceImpl.create(); 86 | const jwtEngine : JwtEngine = jwtService.createJwtEngine(BACKEND_JWT_SECRET, BACKEND_JWT_ALG); 87 | 88 | const emailService : EmailService = EmailServiceImpl.create(BACKEND_EMAIL_FROM); 89 | 90 | const defaultLanguage : Language = parseLanguage(BACKEND_DEFAULT_LANGUAGE) ?? Language.ENGLISH; 91 | 92 | const matrixServer : MatrixServerService = new MatrixServerService( 93 | BACKEND_PUBLIC_URL, 94 | BACKEND_HOSTNAME, 95 | jwtEngine, 96 | parseInteger(BACKEND_ACCESS_TOKEN_EXPIRATION_TIME) 97 | ); 98 | 99 | // Start initializing 100 | 101 | await BackendTranslationServiceImpl.initialize(defaultLanguage, TRANSLATIONS); 102 | 103 | emailService.initialize(BACKEND_EMAIL_CONFIG); 104 | 105 | await matrixServer.initialize(); 106 | 107 | if ( BACKEND_INITIAL_USERS ) { 108 | const users = BACKEND_INITIAL_USERS.split(';'); 109 | LOG.debug(`Creating initial users from "${BACKEND_INITIAL_USERS}": `, users); 110 | let i = 0; 111 | for (; i((resolve, reject) => { 148 | try { 149 | serverListener = server.on(RequestServerImpl.Event.STOPPED, () => { 150 | LOG.debug('Stopping backend server from RequestServer stop event'); 151 | serverListener = undefined; 152 | fedServer.stop(); 153 | resolve(); 154 | }); 155 | } catch(err) { 156 | reject(err); 157 | } 158 | }); 159 | 160 | let fedServerListener : any = undefined; 161 | const fedStopPromise= new Promise((resolve, reject) => { 162 | try { 163 | fedServerListener = fedServer.on(RequestServerImpl.Event.STOPPED, () => { 164 | LOG.debug('Stopping federation server from RequestServer stop event'); 165 | fedServerListener = undefined; 166 | server.stop(); 167 | resolve(); 168 | }); 169 | } catch(err) { 170 | reject(err); 171 | } 172 | }); 173 | 174 | ProcessUtils.setupDestroyHandler( () => { 175 | LOG.debug('Stopping server from process utils event'); 176 | server.stop(); 177 | fedServer.stop(); 178 | if (serverListener) { 179 | serverListener(); 180 | serverListener = undefined; 181 | } 182 | if (fedServerListener) { 183 | fedServerListener(); 184 | fedServerListener = undefined; 185 | } 186 | }, (err : any) => { 187 | LOG.error('Error while shutting down the service: ', err); 188 | }); 189 | 190 | await stopPromise; 191 | await fedStopPromise; 192 | 193 | return CommandExitStatus.OK; 194 | 195 | } catch (err) { 196 | LOG.error(`Fatal error: `, err); 197 | return CommandExitStatus.FATAL_ERROR; 198 | } 199 | 200 | } 201 | 202 | /** 203 | * 204 | * @param scriptName 205 | * @nosideeffects 206 | * @__PURE__ 207 | */ 208 | export function getMainUsage ( 209 | scriptName: string 210 | ): string { 211 | 212 | /* @__PURE__ */if ( /* @__PURE__ */BUILD_WITH_FULL_USAGE ) { 213 | 214 | return `USAGE: ${/* @__PURE__ */scriptName} [OPT(s)] ARG(1) [...ARG(N)] 215 | 216 | HG HomeServer. 217 | 218 | ...and OPT is one of: 219 | 220 | -h --help Print help 221 | -v --version Print version 222 | -- Disables option parsing 223 | 224 | Environment variables: 225 | 226 | BACKEND_LOG_LEVEL as one of: 227 | 228 | ALL 229 | DEBUG 230 | INFO 231 | WARN 232 | ERROR 233 | NONE 234 | `; 235 | } else { 236 | return `USAGE: ${/* @__PURE__ */scriptName} ARG(1) [...ARG(N)] 237 | See ${/* @__PURE__ */BUILD_USAGE_URL} 238 | `; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/controllers/HsBackendController.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022-2023. Heusala Group . All rights reserved. 2 | 3 | import { ReadonlyJsonObject } from "../fi/hg/core/Json"; 4 | import { LogService } from "../fi/hg/core/LogService"; 5 | import { GetMapping } from "../fi/hg/core/request/GetMapping"; 6 | import { PathVariable } from "../fi/hg/core/request/PathVariable"; 7 | import { PostMapping } from "../fi/hg/core/request/PostMapping"; 8 | import { PutMapping } from "../fi/hg/core/request/PutMapping"; 9 | import { RequestBody } from "../fi/hg/core/request/RequestBody"; 10 | import { RequestHeader } from "../fi/hg/core/request/RequestHeader"; 11 | import { RequestMapping } from "../fi/hg/core/request/RequestMapping"; 12 | import { RequestParam } from "../fi/hg/core/request/RequestParam"; 13 | import { ResponseEntity } from "../fi/hg/core/request/types/ResponseEntity"; 14 | import { MATRIX_AUTHORIZATION_HEADER_NAME } from "../fi/hg/matrix/constants/matrix-routes"; 15 | import { RequestParamValueType } from "../fi/hg/core/request/types/RequestParamValueType"; 16 | import { parseMatrixRegisterKind } from "../fi/hg/matrix/types/request/register/types/MatrixRegisterKind"; 17 | import { createSynapsePreRegisterResponseDTO } from "../fi/hg/matrix/types/synapse/SynapsePreRegisterResponseDTO"; 18 | import { isSynapseRegisterRequestDTO } from "../fi/hg/matrix/types/synapse/SynapseRegisterRequestDTO"; 19 | import { createSynapseRegisterResponseDTO, SynapseRegisterResponseDTO } from "../fi/hg/matrix/types/synapse/SynapseRegisterResponseDTO"; 20 | import { createMatrixWhoAmIResponseDTO } from "../fi/hg/matrix/types/response/whoami/MatrixWhoAmIResponseDTO"; 21 | import { isMatrixLoginRequestDTO } from "../fi/hg/matrix/types/request/login/MatrixLoginRequestDTO"; 22 | import { createMatrixLoginResponseDTO, MatrixLoginResponseDTO } from "../fi/hg/matrix/types/response/login/MatrixLoginResponseDTO"; 23 | import { createMatrixDiscoveryInformationDTO } from "../fi/hg/matrix/types/response/login/types/MatrixDiscoveryInformationDTO"; 24 | import { createMatrixHomeServerDTO } from "../fi/hg/matrix/types/response/login/types/MatrixHomeServerDTO"; 25 | import { createMatrixIdentityServerInformationDTO } from "../fi/hg/matrix/types/response/login/types/MatrixIdentityServerInformationDTO"; 26 | import { createGetDirectoryRoomAliasResponseDTO, GetDirectoryRoomAliasResponseDTO } from "../fi/hg/matrix/types/response/directoryRoomAlias/GetDirectoryRoomAliasResponseDTO"; 27 | import { createMatrixRoomJoinedMembersDTO, MatrixRoomJoinedMembersDTO } from "../fi/hg/matrix/types/response/roomJoinedMembers/MatrixRoomJoinedMembersDTO"; 28 | import { createMatrixRoomJoinedMembersRoomMemberDTO } from "../fi/hg/matrix/types/response/roomJoinedMembers/types/MatrixRoomJoinedMembersRoomMemberDTO"; 29 | import { isMatrixMatrixRegisterRequestDTO } from "../fi/hg/matrix/types/request/register/MatrixRegisterRequestDTO"; 30 | import { createMatrixRegisterResponseDTO } from "../fi/hg/matrix/types/response/register/MatrixRegisterResponseDTO"; 31 | import { createGetRoomStateByTypeResponseDTO } from "../fi/hg/matrix/types/response/getRoomStateByType/GetRoomStateByTypeResponseDTO"; 32 | import { isSetRoomStateByTypeRequestDTO } from "../fi/hg/matrix/types/request/setRoomStateByType/SetRoomStateByTypeRequestDTO"; 33 | import { createPutRoomStateWithEventTypeResponseDTO, PutRoomStateWithEventTypeResponseDTO } from "../fi/hg/matrix/types/response/setRoomStateByType/PutRoomStateWithEventTypeResponseDTO"; 34 | import { isMatrixLeaveRoomRequestDTO } from "../fi/hg/matrix/types/request/leaveRoom/MatrixLeaveRoomRequestDTO"; 35 | import { createMatrixLeaveRoomResponseDTO } from "../fi/hg/matrix/types/response/leaveRoom/MatrixLeaveRoomResponseDTO"; 36 | import { isMatrixInviteToRoomRequestDTO } from "../fi/hg/matrix/types/request/inviteToRoom/MatrixInviteToRoomRequestDTO"; 37 | import { createMatrixInviteToRoomResponseDTO } from "../fi/hg/matrix/types/response/inviteToRoom/MatrixInviteToRoomResponseDTO"; 38 | import { isMatrixTextMessageDTO } from "../fi/hg/matrix/types/message/textMessage/MatrixTextMessageDTO"; 39 | import { createSendEventToRoomWithTnxIdResponseDTO } from "../fi/hg/matrix/types/response/sendEventToRoomWithTnxId/SendEventToRoomWithTnxIdResponseDTO"; 40 | import { explainMatrixCreateRoomDTO, isMatrixCreateRoomDTO, MatrixCreateRoomDTO } from "../fi/hg/matrix/types/request/createRoom/MatrixCreateRoomDTO"; 41 | import { createMatrixCreateRoomResponseDTO, MatrixCreateRoomResponseDTO } from "../fi/hg/matrix/types/response/createRoom/MatrixCreateRoomResponseDTO"; 42 | import { isMatrixJoinRoomRequestDTO } from "../fi/hg/matrix/types/request/joinRoom/MatrixJoinRoomRequestDTO"; 43 | import { createMatrixJoinRoomResponseDTO } from "../fi/hg/matrix/types/response/joinRoom/MatrixJoinRoomResponseDTO"; 44 | import { createMatrixSyncResponseDTO, MatrixSyncResponseDTO } from "../fi/hg/matrix/types/response/sync/MatrixSyncResponseDTO"; 45 | import { MatrixServerService } from "../fi/hg/matrix/server/MatrixServerService"; 46 | import { MatrixLoginType } from "../fi/hg/matrix/types/request/login/MatrixLoginType"; 47 | import { createMatrixErrorDTO, isMatrixErrorDTO, MatrixErrorDTO } from "../fi/hg/matrix/types/response/error/MatrixErrorDTO"; 48 | import { MatrixErrorCode } from "../fi/hg/matrix/types/response/error/types/MatrixErrorCode"; 49 | import { MatrixType } from "../fi/hg/matrix/types/core/MatrixType"; 50 | import { AuthorizationUtils } from "../fi/hg/core/AuthorizationUtils"; 51 | import { LogLevel } from "../fi/hg/core/types/LogLevel"; 52 | import { MatrixUtils } from "../fi/hg/matrix/MatrixUtils"; 53 | import { UserRepositoryItem } from "../fi/hg/matrix/server/types/repository/user/UserRepositoryItem"; 54 | import { DeviceRepositoryItem } from "../fi/hg/matrix/server/types/repository/device/DeviceRepositoryItem"; 55 | import { MatrixRoomId } from "../fi/hg/matrix/types/core/MatrixRoomId"; 56 | import { parseMatrixRoomVersion } from "../fi/hg/matrix/types/MatrixRoomVersion"; 57 | import { MatrixVisibility, parseMatrixVisibility } from "../fi/hg/matrix/types/request/createRoom/types/MatrixVisibility"; 58 | import { MatrixRoomCreateEventDTO } from "../fi/hg/matrix/types/event/roomCreate/MatrixRoomCreateEventDTO"; 59 | import { MatrixStateEvent } from "../fi/hg/matrix/types/core/MatrixStateEvent"; 60 | import { MatrixCreateRoomPreset } from "../fi/hg/matrix/types/request/createRoom/types/MatrixCreateRoomPreset"; 61 | import { RoomMembershipState } from "../fi/hg/matrix/types/event/roomMember/RoomMembershipState"; 62 | 63 | const LOG = LogService.createLogger('HsBackendController'); 64 | 65 | export interface WhoAmIResult { 66 | readonly accessToken: string; 67 | readonly userId: string; 68 | readonly deviceId: string; 69 | readonly device: DeviceRepositoryItem; 70 | } 71 | 72 | /** 73 | * Client facing REST backend controller 74 | */ 75 | @RequestMapping("/") 76 | export class HsBackendController { 77 | 78 | private static _matrixServer : MatrixServerService | undefined; 79 | 80 | public static setLogLevel (level: LogLevel) { 81 | LOG.setLogLevel(level); 82 | } 83 | 84 | public static setMatrixServer (value: MatrixServerService) { 85 | this._matrixServer = value; 86 | } 87 | 88 | @GetMapping("/") 89 | public static async getIndex ( 90 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 91 | required: false, 92 | defaultValue: '' 93 | }) 94 | accessHeader: string 95 | ): Promise> { 96 | try { 97 | return ResponseEntity.ok( 98 | { 99 | hello: 'world' 100 | } as unknown as ReadonlyJsonObject 101 | ); 102 | } catch (err) { 103 | return this._handleException('getIndex', err); 104 | } 105 | } 106 | 107 | /** 108 | * @param accessHeader 109 | * @see https://github.com/heusalagroup/hghs/issues/1 110 | */ 111 | @GetMapping("/_synapse/admin/v1/register") 112 | public static async getSynapseAdminRegister ( 113 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 114 | required: false, 115 | defaultValue: '' 116 | }) 117 | accessHeader: string 118 | ): Promise> { 119 | try { 120 | const nonce = await this._matrixServer.createAdminRegisterNonce(); 121 | const response = createSynapsePreRegisterResponseDTO( nonce ); 122 | return ResponseEntity.ok( response as unknown as ReadonlyJsonObject ); 123 | } catch (err) { 124 | return this._handleException('getSynapseAdminRegister', err); 125 | } 126 | } 127 | 128 | /** 129 | * 130 | * @param accessHeader 131 | * @param body 132 | * @see https://github.com/heusalagroup/hghs/issues/1 133 | */ 134 | @PostMapping("/_synapse/admin/v1/register") 135 | public static async postSynapseAdminRegister ( 136 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 137 | required: false, 138 | defaultValue: '' 139 | }) 140 | accessHeader: string, 141 | @RequestBody 142 | body: ReadonlyJsonObject 143 | ): Promise> { 144 | try { 145 | if ( !isSynapseRegisterRequestDTO(body) ) { 146 | return ResponseEntity.badRequest().body( 147 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not AuthenticateEmailDTO`) 148 | ).status(400); 149 | } 150 | // @FIXME: Implement the end point 151 | const response : SynapseRegisterResponseDTO = createSynapseRegisterResponseDTO( 152 | 'access_token', 153 | 'user_id', 154 | 'home_server', 155 | 'device_id' 156 | ); 157 | return ResponseEntity.ok( response as unknown as ReadonlyJsonObject ); 158 | } catch (err) { 159 | return this._handleException('postSynapseAdminRegister', err); 160 | } 161 | } 162 | 163 | /** 164 | * 165 | * @param accessHeader 166 | * @see https://github.com/heusalagroup/hghs/issues/2 167 | */ 168 | @GetMapping("/_matrix/client/r0/account/whoami") 169 | public static async accountWhoAmI ( 170 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 171 | required: false, 172 | defaultValue: '' 173 | }) 174 | accessHeader: string 175 | ): Promise> { 176 | try { 177 | LOG.debug(`accountWhoAmI: accessHeader = `, accessHeader); 178 | 179 | const {userId, deviceId, device} = await this._whoAmIFromAccessHeader(accessHeader); 180 | 181 | const user : UserRepositoryItem | undefined = await this._matrixServer.findUserById(userId); 182 | LOG.debug(`whoAmI: user = `, user); 183 | if (!user) { 184 | LOG.warn(`whoAmI: User not found: `, user, userId, deviceId); 185 | return ResponseEntity.badRequest().body( 186 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN_TOKEN,'Unrecognised access token.') 187 | ).status(401); 188 | } 189 | 190 | const username = user?.username; 191 | LOG.debug(`whoAmI: username = `, username); 192 | 193 | const deviceIdentifier = device?.deviceId ?? device?.id; 194 | 195 | const dto = createMatrixWhoAmIResponseDTO( 196 | MatrixUtils.getUserId(username, this._matrixServer.getHostName()), 197 | deviceIdentifier ? deviceIdentifier : undefined, 198 | false 199 | ); 200 | 201 | LOG.debug(`accountWhoAmI: response = `, dto); 202 | return ResponseEntity.ok( dto as unknown as ReadonlyJsonObject ); 203 | 204 | } catch (err) { 205 | return this._handleException('accountWhoAmI', err); 206 | } 207 | } 208 | 209 | /** 210 | * 211 | * @param accessHeader 212 | * @param body 213 | * @see https://github.com/heusalagroup/hghs/issues/3 214 | */ 215 | @PostMapping("/_matrix/client/r0/login") 216 | public static async login ( 217 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 218 | required: false, 219 | defaultValue: '' 220 | }) 221 | accessHeader: string, 222 | @RequestBody 223 | body: ReadonlyJsonObject 224 | ): Promise> { 225 | try { 226 | 227 | if (!isMatrixLoginRequestDTO(body)) { 228 | return ResponseEntity.badRequest().body( 229 | createMatrixErrorDTO(MatrixErrorCode.M_FORBIDDEN, `Body not MatrixLoginRequestDTO`) 230 | ).status(400); 231 | } 232 | 233 | if (body?.type !== MatrixLoginType.M_LOGIN_PASSWORD) { 234 | return ResponseEntity.badRequest().body( 235 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Only type ${MatrixLoginType.M_LOGIN_PASSWORD} supported`) 236 | ).status(400); 237 | } 238 | 239 | if ( body?.identifier && body?.identifier?.type !== MatrixType.M_ID_USER ) { 240 | return ResponseEntity.badRequest().body( 241 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Only identifier type ${MatrixType.M_ID_USER} supported`) 242 | ).status(400); 243 | } 244 | 245 | const user = body?.user ?? body?.identifier?.user; 246 | const password = body?.password; 247 | 248 | if ( !user || !password ) { 249 | return ResponseEntity.badRequest().body( 250 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`User or password property not defined`) 251 | ).status(400); 252 | } 253 | 254 | const deviceId = body?.device_id; 255 | 256 | const accessToken = await this._matrixServer.loginWithPassword(user, password, deviceId); 257 | if (!accessToken) { 258 | return ResponseEntity.badRequest().body( 259 | createMatrixErrorDTO(MatrixErrorCode.M_FORBIDDEN, `Access denied`) 260 | ).status(403); 261 | } 262 | 263 | const backendHostname = this._matrixServer.getHostName(); 264 | const backendUrl = this._matrixServer.getURL(); 265 | 266 | // @FIXME: Implement https://github.com/heusalagroup/hghs/issues/3 267 | const responseDto : MatrixLoginResponseDTO = createMatrixLoginResponseDTO( 268 | MatrixUtils.getUserId(user, backendHostname), 269 | accessToken, 270 | backendUrl, 271 | deviceId, 272 | createMatrixDiscoveryInformationDTO( 273 | createMatrixHomeServerDTO(backendUrl), 274 | createMatrixIdentityServerInformationDTO(backendUrl) 275 | ) 276 | ); 277 | 278 | return ResponseEntity.ok( responseDto as unknown as ReadonlyJsonObject ); 279 | 280 | } catch (err) { 281 | return this._handleException('login', err); 282 | } 283 | } 284 | 285 | /** 286 | * 287 | * @param accessHeader 288 | * @param roomAlias 289 | * @see https://github.com/heusalagroup/hghs/issues/4 290 | */ 291 | @GetMapping("/_matrix/client/r0/directory/room/:roomAlias") 292 | public static async getDirectoryRoomByName ( 293 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 294 | required: false, 295 | defaultValue: '' 296 | }) 297 | accessHeader: string, 298 | @PathVariable('roomAlias', {required: true}) 299 | roomAlias = "" 300 | ): Promise> { 301 | try { 302 | LOG.debug(`getDirectoryRoomByName: roomAlias = `, roomAlias); 303 | const response : GetDirectoryRoomAliasResponseDTO = createGetDirectoryRoomAliasResponseDTO( 304 | 'room_id', 305 | ['server1'] 306 | ); 307 | return ResponseEntity.ok( 308 | response as unknown as ReadonlyJsonObject 309 | ); 310 | } catch (err) { 311 | return this._handleException('getDirectoryRoomByName', err); 312 | } 313 | } 314 | 315 | /** 316 | * 317 | * @param accessHeader 318 | * @param roomId 319 | * @see https://github.com/heusalagroup/hghs/issues/5 320 | */ 321 | @GetMapping("/_matrix/client/r0/rooms/:roomId/joined_members") 322 | public static async getRoomJoinedMembers ( 323 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 324 | required: false, 325 | defaultValue: '' 326 | }) 327 | accessHeader: string, 328 | @PathVariable('roomId', {required: true}) 329 | roomId = "" 330 | ): Promise> { 331 | try { 332 | 333 | LOG.debug(`getRoomJoinedMembers: roomId = `, roomId); 334 | 335 | const responseDto : MatrixRoomJoinedMembersDTO = createMatrixRoomJoinedMembersDTO( 336 | { 337 | "user": createMatrixRoomJoinedMembersRoomMemberDTO("display_name", "avatar_url") 338 | } 339 | ); 340 | 341 | return ResponseEntity.ok( 342 | responseDto as unknown as ReadonlyJsonObject 343 | ); 344 | 345 | } catch (err) { 346 | return this._handleException('getRoomJoinedMembers', err); 347 | } 348 | } 349 | 350 | /** 351 | * 352 | * @param accessHeader 353 | * @param kindString 354 | * @param body 355 | * @see https://github.com/heusalagroup/hghs/issues/6 356 | */ 357 | @PostMapping("/_matrix/client/r0/register") 358 | public static async registerUser ( 359 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 360 | required: false, 361 | defaultValue: '' 362 | }) 363 | accessHeader: string, 364 | @RequestParam('kind', RequestParamValueType.STRING) 365 | kindString = "", 366 | @RequestBody 367 | body: ReadonlyJsonObject 368 | ): Promise> { 369 | try { 370 | 371 | const kind : string | undefined = parseMatrixRegisterKind(kindString); 372 | LOG.debug(`registerUser: kind = `, kind); 373 | 374 | if (!isMatrixMatrixRegisterRequestDTO(body)) { 375 | return ResponseEntity.badRequest().body( 376 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not MatrixMatrixRegisterRequestDTO`) 377 | ).status(400); 378 | } 379 | 380 | // @FIXME: Implement https://github.com/heusalagroup/hghs/issues/6 381 | const responseDto = createMatrixRegisterResponseDTO( 382 | 'user_id', 383 | 'access_token', 384 | 'home_server', 385 | 'device_id' 386 | ); 387 | 388 | return ResponseEntity.ok( 389 | responseDto as unknown as ReadonlyJsonObject 390 | ); 391 | 392 | } catch (err) { 393 | return this._handleException('registerUser', err); 394 | } 395 | } 396 | 397 | /** 398 | * 399 | * @param accessHeader 400 | * @param roomId 401 | * @param eventType 402 | * @param stateKey 403 | * @see https://github.com/heusalagroup/hghs/issues/7 404 | */ 405 | @GetMapping("/_matrix/client/r0/rooms/:roomId/state/:eventType/:stateKey") 406 | public static async getRoomStateByType ( 407 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 408 | required: false, 409 | defaultValue: '' 410 | }) 411 | accessHeader: string, 412 | @PathVariable('roomId', {required: true}) 413 | roomId = "", 414 | @PathVariable('eventType', {required: true}) 415 | eventType = "", 416 | @PathVariable('stateKey', {required: true}) 417 | stateKey = "" 418 | ): Promise> { 419 | try { 420 | LOG.debug(`getRoomStateByType: roomId = `, roomId, eventType, stateKey); 421 | const responseDto = createGetRoomStateByTypeResponseDTO('roomName'); 422 | return ResponseEntity.ok( 423 | responseDto as unknown as ReadonlyJsonObject 424 | ); 425 | } catch (err) { 426 | return this._handleException('getRoomStateByType', err); 427 | } 428 | } 429 | 430 | /** 431 | * 432 | * @param accessHeader 433 | * @param roomId 434 | * @param eventType 435 | * @param stateKey 436 | * @param body 437 | * @see https://github.com/heusalagroup/hghs/issues/8 438 | */ 439 | @PutMapping("/_matrix/client/r0/rooms/:roomId/state/:eventType/:stateKey") 440 | public static async setRoomStateByType ( 441 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 442 | required: false, 443 | defaultValue: '' 444 | }) 445 | accessHeader: string, 446 | @PathVariable('roomId', {required: true}) 447 | roomId = "", 448 | @PathVariable('eventType', {required: true}) 449 | eventType = "", 450 | @PathVariable('stateKey', {required: true}) 451 | stateKey = "", 452 | @RequestBody 453 | body: ReadonlyJsonObject 454 | ): Promise> { 455 | try { 456 | 457 | LOG.debug(`setRoomStateByType: roomId = `, roomId, eventType, stateKey); 458 | 459 | if (!isSetRoomStateByTypeRequestDTO(body)) { 460 | return ResponseEntity.badRequest().body( 461 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not SetRoomStateByTypeRequestDTO`) 462 | ).status(400); 463 | } 464 | 465 | // @todo: Implement https://github.com/heusalagroup/hghs/issues/8 466 | const responseDto : PutRoomStateWithEventTypeResponseDTO = createPutRoomStateWithEventTypeResponseDTO( 467 | eventType 468 | ); 469 | 470 | return ResponseEntity.ok( 471 | responseDto as unknown as ReadonlyJsonObject 472 | ); 473 | 474 | } catch (err) { 475 | return this._handleException('setRoomStateByType', err); 476 | } 477 | } 478 | 479 | /** 480 | * 481 | * @param accessHeader 482 | * @param roomId 483 | * @see https://github.com/heusalagroup/hghs/issues/9 484 | */ 485 | @PostMapping("/_matrix/client/r0/rooms/:roomId/forget") 486 | public static async forgetRoom ( 487 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 488 | required: false, 489 | defaultValue: '' 490 | }) 491 | accessHeader: string, 492 | @PathVariable('roomId', {required: true}) 493 | roomId = "" 494 | ): Promise> { 495 | try { 496 | LOG.debug(`forgetRoom: roomId = `, roomId); 497 | // @FIXME: Implement https://github.com/heusalagroup/hghs/issues/9 498 | return ResponseEntity.ok( 499 | {} as unknown as ReadonlyJsonObject 500 | ); 501 | } catch (err) { 502 | return this._handleException('forgetRoom', err); 503 | } 504 | } 505 | 506 | /** 507 | * 508 | * @param accessHeader 509 | * @param roomId 510 | * @param body 511 | * @see https://github.com/heusalagroup/hghs/issues/10 512 | */ 513 | @PostMapping("/_matrix/client/r0/rooms/:roomId/leave") 514 | public static async leaveRoom ( 515 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 516 | required: false, 517 | defaultValue: '' 518 | }) 519 | accessHeader: string, 520 | @PathVariable('roomId', {required: true}) 521 | roomId = "", 522 | @RequestBody 523 | body: ReadonlyJsonObject 524 | ): Promise> { 525 | try { 526 | LOG.debug(`leaveRoom: roomId = `, roomId); 527 | if (!isMatrixLeaveRoomRequestDTO(body)) { 528 | return ResponseEntity.badRequest().body( 529 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not MatrixLeaveRoomRequestDTO`) 530 | ).status(400); 531 | } 532 | const responseDto = createMatrixLeaveRoomResponseDTO(); 533 | return ResponseEntity.ok( 534 | responseDto as unknown as ReadonlyJsonObject 535 | ); 536 | } catch (err) { 537 | return this._handleException('leaveRoom', err); 538 | } 539 | } 540 | 541 | /** 542 | * 543 | * @param accessHeader 544 | * @param roomId 545 | * @param body 546 | * @see https://github.com/heusalagroup/hghs/issues/11 547 | */ 548 | @PostMapping("/_matrix/client/r0/rooms/:roomId/invite") 549 | public static async inviteToRoom ( 550 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 551 | required: false, 552 | defaultValue: '' 553 | }) 554 | accessHeader: string, 555 | @PathVariable('roomId', {required: true}) 556 | roomId = "", 557 | @RequestBody 558 | body: ReadonlyJsonObject 559 | ): Promise> { 560 | try { 561 | LOG.debug(`inviteToRoom: roomId = `, roomId); 562 | if (!isMatrixInviteToRoomRequestDTO(body)) { 563 | return ResponseEntity.badRequest().body( 564 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not MatrixInviteToRoomRequestDTO`) 565 | ).status(400); 566 | } 567 | // FIXME: Implement https://github.com/heusalagroup/hghs/issues/11 568 | const responseDto = createMatrixInviteToRoomResponseDTO(); 569 | return ResponseEntity.ok( 570 | responseDto as unknown as ReadonlyJsonObject 571 | ); 572 | } catch (err) { 573 | return this._handleException('inviteToRoom', err); 574 | } 575 | } 576 | 577 | /** 578 | * 579 | * @param accessHeader 580 | * @param roomId 581 | * @param eventName 582 | * @param tnxId 583 | * @param body 584 | * @see https://github.com/heusalagroup/hghs/issues/12 585 | */ 586 | @PutMapping("/_matrix/client/v3/rooms/:roomId/send/:eventName/:tnxId") 587 | public static async sendEventToRoomWithTnxId ( 588 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 589 | required: false, 590 | defaultValue: '' 591 | }) 592 | accessHeader: string, 593 | @PathVariable('roomId', {required: true}) 594 | roomId = "", 595 | @PathVariable('eventName', {required: true}) 596 | eventName = "", 597 | @PathVariable('tnxId', {required: true}) 598 | tnxId = "", 599 | @RequestBody 600 | body: ReadonlyJsonObject 601 | ): Promise> { 602 | try { 603 | LOG.debug(`sendEventToRoomWithTnxId: roomId = `, roomId, eventName, tnxId); 604 | if (!isMatrixTextMessageDTO(body)) { 605 | return ResponseEntity.badRequest().body( 606 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not MatrixTextMessageDTO`) 607 | ).status(400); 608 | } 609 | // TODO: Implement https://github.com/heusalagroup/hghs/issues/12 610 | const responseDto = createSendEventToRoomWithTnxIdResponseDTO('event_id'); 611 | return ResponseEntity.ok( 612 | responseDto as unknown as ReadonlyJsonObject 613 | ); 614 | } catch (err) { 615 | return this._handleException('sendEventToRoomWithTnxId', err); 616 | } 617 | } 618 | 619 | /** 620 | * 621 | * @param accessHeader 622 | * @param bodyJson 623 | * @see https://github.com/heusalagroup/hghs/issues/13 624 | */ 625 | @PostMapping("/_matrix/client/r0/createRoom") 626 | public static async createRoom ( 627 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 628 | required: false, 629 | defaultValue: '' 630 | }) 631 | accessHeader: string, 632 | @RequestBody 633 | bodyJson: ReadonlyJsonObject 634 | ): Promise> { 635 | try { 636 | 637 | LOG.debug(`createRoom: bodyJson = `, bodyJson); 638 | if (!isMatrixCreateRoomDTO(bodyJson)) { 639 | LOG.debug(`Body invalid: ${explainMatrixCreateRoomDTO(bodyJson)}`); 640 | return ResponseEntity.badRequest().body( 641 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not MatrixCreateRoomDTO`) 642 | ).status(400); 643 | } 644 | const body : MatrixCreateRoomDTO = bodyJson; 645 | 646 | LOG.debug(`createRoom: accessHeader = `, accessHeader); 647 | const { userId, deviceId } = await this._whoAmIFromAccessHeader(accessHeader); 648 | 649 | const creationContent : Partial | undefined = body?.creation_content; 650 | 651 | const visibility : MatrixVisibility = parseMatrixVisibility(body?.visibility) ?? MatrixVisibility.PRIVATE; 652 | const roomVersion = parseMatrixRoomVersion(body?.room_version) ?? this._matrixServer.getDefaultRoomVersion(); 653 | 654 | const preset : MatrixCreateRoomPreset = body?.preset ?? MatrixUtils.getRoomPresetFromVisibility(visibility); 655 | 656 | LOG.debug(`createRoom: whoAmI: `, userId, deviceId); 657 | const {roomId} = await this._matrixServer.createRoom(userId, deviceId, roomVersion, visibility); 658 | 659 | const presetStateEvents = MatrixUtils.getRoomStateEventsFromPreset(roomId, preset); 660 | 661 | const initialStateEvents : readonly MatrixStateEvent[] = body?.initial_state ?? []; 662 | 663 | // 1. Add m.room.create event 664 | await this._matrixServer.createRoomCreateEvent( 665 | userId, 666 | roomId, 667 | roomVersion, 668 | userId, 669 | creationContent 670 | ); 671 | 672 | // 2. Create m.room.member event 673 | await this._matrixServer.createRoomMemberEvent( 674 | userId, 675 | roomId, 676 | RoomMembershipState.JOIN 677 | ); 678 | 679 | // 3. TODO: Create `m.room.power_levels` event 680 | // 4. TODO: Create `m.room.canonical_alias` event if `body.room_alias_name` is defined 681 | // 5. TODO: Add events from `presetStateEvents` 682 | 683 | // 6. Add events from `initialStateEvents` 684 | let i = 0; 685 | for (; i(responseDto); 715 | 716 | } catch (err) { 717 | return this._handleException('createRoom', err); 718 | } 719 | } 720 | 721 | /** 722 | * 723 | * @param accessHeader 724 | * @param roomId 725 | * @param body 726 | * @see https://github.com/heusalagroup/hghs/issues/14 727 | */ 728 | @PostMapping("/_matrix/client/r0/rooms/:roomId/join") 729 | public static async joinToRoom ( 730 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 731 | required: false, 732 | defaultValue: '' 733 | }) 734 | accessHeader: string, 735 | @PathVariable('roomId', {required: true}) 736 | roomId = "", 737 | @RequestBody 738 | body: ReadonlyJsonObject 739 | ): Promise> { 740 | try { 741 | if (!isMatrixJoinRoomRequestDTO(body)) { 742 | return ResponseEntity.badRequest().body( 743 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN,`Body not MatrixJoinRoomRequestDTO`) 744 | ).status(400); 745 | } 746 | LOG.debug(`joinToRoom: `, body); 747 | // FIXME: Implement https://github.com/heusalagroup/hghs/issues/14 748 | const responseDto = createMatrixJoinRoomResponseDTO('room_id'); 749 | return ResponseEntity.ok( 750 | responseDto as unknown as ReadonlyJsonObject 751 | ); 752 | } catch (err) { 753 | return this._handleException('joinToRoom', err); 754 | } 755 | } 756 | 757 | /** 758 | * 759 | * @param accessHeader 760 | * @param filter 761 | * @param since 762 | * @param full_state 763 | * @param set_presence 764 | * @param timeout 765 | * @see https://github.com/heusalagroup/hghs/issues/15 766 | */ 767 | @GetMapping("/_matrix/client/r0/sync") 768 | public static async sync ( 769 | @RequestHeader(MATRIX_AUTHORIZATION_HEADER_NAME, { 770 | required: false, 771 | defaultValue: '' 772 | }) 773 | accessHeader: string, 774 | @RequestParam('filter', RequestParamValueType.STRING) 775 | filter = "", 776 | @RequestParam('since', RequestParamValueType.STRING) 777 | since = "", 778 | @RequestParam('full_state', RequestParamValueType.STRING) 779 | full_state = "", 780 | @RequestParam('set_presence', RequestParamValueType.STRING) 781 | set_presence = "", 782 | @RequestParam('timeout', RequestParamValueType.STRING) 783 | timeout = "" 784 | ): Promise> { 785 | try { 786 | LOG.debug(`sync: `, filter, since, full_state, set_presence, timeout); 787 | const responseDto : MatrixSyncResponseDTO = createMatrixSyncResponseDTO( 788 | 'next_batch' 789 | ); 790 | return ResponseEntity.ok( 791 | responseDto as unknown as ReadonlyJsonObject 792 | ); 793 | } catch (err) { 794 | return this._handleException('sync', err); 795 | } 796 | } 797 | 798 | /** 799 | * Verifies who is the requester using access token 800 | * 801 | * @param accessToken 802 | * @private 803 | */ 804 | private static async _whoAmIFromAccessToken (accessToken: string) : Promise { 805 | 806 | LOG.debug(`whoAmI: accessToken = `, accessToken); 807 | if ( !accessToken ) { 808 | LOG.warn(`Warning! No authentication token provided.`); 809 | throw createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN_TOKEN, 'Unrecognised access token.'); 810 | } 811 | 812 | const deviceId: string | undefined = await this._matrixServer.verifyAccessToken(accessToken); 813 | if (!deviceId) { 814 | throw createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN_TOKEN,'Unrecognised access token.') ; 815 | } 816 | 817 | const device = await this._matrixServer.findDeviceById(deviceId); 818 | if (!device) { 819 | LOG.warn(`whoAmI: Device not found: `, deviceId, accessToken); 820 | throw createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN_TOKEN,'Unrecognised access token.'); 821 | } 822 | 823 | const userId = device?.userId; 824 | LOG.debug(`whoAmI: userId = `, userId); 825 | if (!userId) { 826 | LOG.warn(`whoAmI: User ID invalid: `, userId, deviceId, accessToken); 827 | throw createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN_TOKEN,'Unrecognised access token.'); 828 | } 829 | 830 | return { 831 | accessToken, 832 | deviceId, 833 | device, 834 | userId 835 | }; 836 | 837 | } 838 | 839 | /** 840 | * Verifies who is the requester using Bearer auth access header value 841 | * 842 | * @param accessHeader 843 | * @private 844 | */ 845 | private static async _whoAmIFromAccessHeader (accessHeader: string) : Promise { 846 | LOG.debug(`_whoAmIFromAccessHeader: accessHeader = `, accessHeader); 847 | const accessToken = AuthorizationUtils.parseBearerToken(accessHeader); 848 | LOG.debug(`_whoAmIFromAccessHeader: accessToken = `, accessToken); 849 | return this._whoAmIFromAccessToken(accessToken); 850 | } 851 | 852 | /** 853 | * Handle exceptions 854 | * 855 | * @param callName 856 | * @param err 857 | * @private 858 | */ 859 | private static _handleException (callName: string, err: any) : ResponseEntity { 860 | LOG.error(`${callName}: ERROR: `, err); 861 | if (isMatrixErrorDTO(err)) { 862 | return ResponseEntity.badRequest().body(err).status(401); 863 | } 864 | return ResponseEntity.internalServerError().body( 865 | createMatrixErrorDTO(MatrixErrorCode.M_UNKNOWN, 'Internal Server Error') 866 | ); 867 | } 868 | 869 | } 870 | --------------------------------------------------------------------------------