commit 1f20a611da8071c6a273f66a9f8711a5d9548dd71245c1d5c45fcf157470a798 Author: aslan Date: Tue Dec 23 07:18:10 2025 -0500 Initial code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7d0f78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/auth.json b/auth.json new file mode 100644 index 0000000..18e3f1a --- /dev/null +++ b/auth.json @@ -0,0 +1 @@ +{"secretKey":"XT0HBMKvfXV9zp8r1CFuIHU4XWLps6qkfabacrxdkNQ="} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8236d1c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,432 @@ +{ + "name": "aslobot-matrix", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aslobot-matrix", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "matrix-js-sdk": "^39.4.0" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-15.3.0.tgz", + "integrity": "sha512-QyxHvncvkl7nf+tnn92PjQ54gMNV8hMSpiukiDgNrqF6IYwgySTlcSdkPYdw8QjZJ0NR6fnVrNzMec0OohM3wA==", + "license": "Apache-2.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/another-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", + "integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==", + "license": "Apache-2.0" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/matrix-events-sdk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", + "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==", + "license": "Apache-2.0" + }, + "node_modules/matrix-js-sdk": { + "version": "39.4.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-39.4.0.tgz", + "integrity": "sha512-0RZLcwbMxMTU+ORPhpFUnX8nDbKwLtdW20T6VesSxEwjKL5j2TM/mIt4u3h0HJiMh63PpAb2QUcNr0R82YfTOA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@matrix-org/matrix-sdk-crypto-wasm": "^15.3.0", + "another-json": "^0.2.0", + "bs58": "^6.0.0", + "content-type": "^1.0.4", + "jwt-decode": "^4.0.0", + "loglevel": "^1.9.2", + "matrix-events-sdk": "0.0.1", + "matrix-widget-api": "^1.14.0", + "oidc-client-ts": "^3.0.1", + "p-retry": "7", + "sdp-transform": "^3.0.0", + "unhomoglyph": "^1.0.6", + "uuid": "13" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/matrix-widget-api": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.15.0.tgz", + "integrity": "sha512-Yu9rX9wyF3A1sqviKgiYHz8aGgL3HhJe9OXKi/lccr1eZnNb6y+ELdbshTjs+VLKM4rkTWt6CE3THsw3f/CZhg==", + "license": "Apache-2.0", + "dependencies": { + "@types/events": "^3.0.0", + "events": "^3.2.0" + } + }, + "node_modules/oidc-client-ts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz", + "integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==", + "license": "Apache-2.0", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sdp-transform": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", + "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==", + "license": "MIT", + "bin": { + "sdp-verify": "checker.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unhomoglyph": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", + "integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd79cf8 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "aslobot-matrix", + "version": "0.1.0", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "index.js", + "scripts": { + "build": "npx tsc", + "start": "npx tsc && node dist/index.js" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "matrix-js-sdk": "^39.4.0" + } +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts new file mode 100644 index 0000000..d11f312 --- /dev/null +++ b/src/auth/auth.ts @@ -0,0 +1,33 @@ +import { existsSync, readFileSync, writeFileSync } from "fs"; +import crypto from "crypto"; +import type { BotAuth, BotAuthJson } from "./types.js"; + +const newAuth = (): BotAuth => { + return { + secretKey: new Uint8Array(crypto.randomBytes(32)), + }; +}; + +const saveAuth = (authPath: string, auth: BotAuth) => { + const authJson: BotAuthJson = { + secretKey: Buffer.from(auth.secretKey).toString("base64"), + }; + const json = JSON.stringify(authJson); + + writeFileSync(authPath, json); +}; + +const loadAuth = (authPath: string): BotAuth => { + if (!existsSync(authPath)) { + saveAuth(authPath, newAuth()); + } + + const json = readFileSync(authPath).toString(); + const authJson = JSON.parse(json) as BotAuthJson; + + return { + secretKey: new Uint8Array(Buffer.from(authJson.secretKey, "base64")), + }; +}; + +export { newAuth, saveAuth, loadAuth }; diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..9561b83 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,2 @@ +export * from "./auth.js"; +export * from "./types.js"; diff --git a/src/auth/types.ts b/src/auth/types.ts new file mode 100644 index 0000000..ddd2340 --- /dev/null +++ b/src/auth/types.ts @@ -0,0 +1,9 @@ +interface BotAuth { + secretKey: Uint8Array; +} + +interface BotAuthJson { + secretKey: string; +} + +export { type BotAuth, type BotAuthJson }; diff --git a/src/config.json b/src/config.json new file mode 100644 index 0000000..dd02bfa --- /dev/null +++ b/src/config.json @@ -0,0 +1,18 @@ +{ + "baseUrl": "https://matrix.aslan2142.space", + "userId": "@aslobot:aslan2142.space", + "authPath": "auth.json", + "storePath": "store.json", + "auth": { + "accessToken": "mct_iW6Cif22H34s5yAHrmqfBQUsMrGaH2_0QWfYU", + "deviceId": "PBz1Ig9c3p" + }, + "app": { + "triggerPrefix": "!", + "experience": { + "gain": 5, + "startingRequirement": 50, + "timeout": 60000 + } + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..faee836 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,3 @@ +import config from "./config.json" with { type: "json" }; + +export { config }; diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..473a0f4 diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..3da5644 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,51 @@ +import { config } from "./config.js"; +import { state } from "./store/store.js"; +import { type IUser, type TRole } from "./store/types.js"; +import type { IRank } from "./types.js"; + +const getUserById = (userId: string): IUser => { + return ( + state.users.find((user) => user.id === userId) ?? { + id: ":", + role: "NONE", + experience: 0, + lastMessageTimestamp: 0, + } + ); +}; + +const checkRoles = (roles: TRole[], userId: string) => { + const user = getUserById(userId); + return roles.includes(user.role); +}; + +const getRank = (experience: number): IRank => { + let tmpExperience = experience; + let expToNextRank = config.app.experience.startingRequirement; + let rank = 0; + + while (tmpExperience >= expToNextRank) { + rank++; + tmpExperience -= expToNextRank; + expToNextRank = expToNextRank *= 1.2; + } + + return { + rank: rank, + experience: Math.floor(experience), + experienceInRank: Math.floor(tmpExperience), + expToNextRank: Math.floor(expToNextRank), + }; +}; + +const getUserName = (user: IUser): string => { + const userPattern = /@[a-zA-Z0-9]*/; + const match = user.id.match(userPattern)?.at(0); + if (!match) { + return ""; + } + + return match.replaceAll("@", ""); +}; + +export { getUserById, checkRoles, getRank, getUserName }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..98922ef --- /dev/null +++ b/src/index.ts @@ -0,0 +1,41 @@ +import config from "./config.json" with { type: "json" }; + +import { MatrixClient, createClient, ClientEvent } from "matrix-js-sdk"; +import { registerModules } from "./modules/module.js"; + +let matrixClient: MatrixClient | undefined = undefined; + +const initialize = async (): Promise => { + matrixClient = createClient({ + baseUrl: config.baseUrl, + userId: config.userId, + accessToken: config.auth.accessToken, + deviceId: config.auth.deviceId, + }); + + matrixClient.once(ClientEvent.Sync, function (state) { + if (state === "PREPARED") { + console.log("prepared"); + } + }); + + matrixClient.startClient({ initialSyncLimit: 1 }); + + return 0; +}; + +const listen = async () => { + if (!matrixClient) { + return; + } + + registerModules(matrixClient); +}; + +const initCode = await initialize(); + +if (initCode > 0) { + process.exit(initCode); +} + +listen(); diff --git a/src/modules/admin/admin.ts b/src/modules/admin/admin.ts new file mode 100644 index 0000000..6aec9cc --- /dev/null +++ b/src/modules/admin/admin.ts @@ -0,0 +1,47 @@ +import { MatrixClient } from "matrix-js-sdk"; +import type { ICallbackStore } from "../types.js"; +import { config } from "../../config.js"; +import { load, save } from "../../store/store.js"; + +let client: MatrixClient; + +const registerModuleAdmin = ( + matrixClient: MatrixClient, + callbackStore: ICallbackStore, +) => { + client = matrixClient; + + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}shutdown`, + allowedRoles: ["ADMIN"], + callbackFunc: onShutdown, + }); + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}loaddata`, + allowedRoles: ["MODERATOR", "ADMIN"], + callbackFunc: onLoadData, + }); + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}savedata`, + allowedRoles: ["MODERATOR", "ADMIN"], + callbackFunc: onSaveData, + }); +}; + +const onShutdown = (text: string) => { + if (!text.includes("nosave")) { + save(); + } + + process.exit(0); +}; + +const onLoadData = () => { + load(); +}; + +const onSaveData = () => { + save(); +}; + +export { registerModuleAdmin }; diff --git a/src/modules/admin/index.ts b/src/modules/admin/index.ts new file mode 100644 index 0000000..6588af2 --- /dev/null +++ b/src/modules/admin/index.ts @@ -0,0 +1 @@ +export * from "./admin.js"; diff --git a/src/modules/base/base.ts b/src/modules/base/base.ts new file mode 100644 index 0000000..9d4fd66 --- /dev/null +++ b/src/modules/base/base.ts @@ -0,0 +1,63 @@ +import { MatrixClient } from "matrix-js-sdk"; +import type { ICallbackStore } from "../types.js"; +import { config } from "../../config.js"; + +let client: MatrixClient; + +const registerModuleTest = ( + matrixClient: MatrixClient, + callbackStore: ICallbackStore, +) => { + client = matrixClient; + + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}ping`, + callbackFunc: onPing, + }); + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}say `, + callbackFunc: onSay, + }); + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}help`, + callbackFunc: onHelp, + }); +}; + +const onPing = (_text: string, roomId: string) => { + client.sendTextMessage(roomId, "Pong!"); +}; + +const onSay = (text: string, roomId: string) => { + const trigger = `${config.app.triggerPrefix}say `; + + client.sendTextMessage(roomId, text.replace(trigger, "")); +}; + +const onHelp = (_text: string, roomId: string) => { + client.sendHtmlMessage( + roomId, + "", + `

Role: User

+
    +
  • !ping - Pong!
  • +
  • !say {text} - Repeats your message
  • +
  • !help - Prints this help message
  • +
  • !rank - Prints your rank and experience
  • +
  • !leaderboard - Prints total user ranking
  • +
+
+

Role: Moderator

+
    +
  • !load - Load bot data
  • +
  • !save - Save bot data
  • +
+
+

Role: Admin

+
    +
  • !shutdown - Shutdown bot
  • +
`, + ); +}; + +export { registerModuleTest }; diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts new file mode 100644 index 0000000..2d3ba1f --- /dev/null +++ b/src/modules/base/index.ts @@ -0,0 +1 @@ +export * from "./base.js"; diff --git a/src/modules/global.ts b/src/modules/global.ts new file mode 100644 index 0000000..4deef74 --- /dev/null +++ b/src/modules/global.ts @@ -0,0 +1,55 @@ +import type { MatrixClient } from "matrix-js-sdk"; +import type { TRole } from "../store/types.js"; +import { getRank, getUserById } from "../helpers.js"; +import { config } from "../config.js"; +import { state } from "../store/store.js"; + +const onAnyMessage = ( + client: MatrixClient, + _text: string, + roomId: string, + sender: string, +) => { + const date = Date.now(); + + const user = getUserById(sender); + if (user.id === ":") { + state.users.push({ + id: sender, + role: "USER", + experience: 0, + lastMessageTimestamp: date, + }); + return onAnyMessage(client, _text, roomId, sender); + } + + const rankBefore = getRank(user.experience); + + if (date > user.lastMessageTimestamp + config.app.experience.timeout) { + user.experience += config.app.experience.gain; + } + user.lastMessageTimestamp = date; + + const rankAfter = getRank(user.experience); + if (rankAfter.rank > rankBefore.rank) { + client.sendHtmlMessage( + roomId, + "", + `${sender} - You are now rank ${rankAfter.rank}`, + ); + } +}; + +const onMissingRole = ( + client: MatrixClient, + userRole: TRole, + roomId: string, +) => { + client.sendHtmlMessage( + roomId, + "", + `You are missing the required role.
Your current role is ${userRole}`, + ); +}; + +export { onAnyMessage, onMissingRole }; diff --git a/src/modules/module.ts b/src/modules/module.ts new file mode 100644 index 0000000..86b7d92 --- /dev/null +++ b/src/modules/module.ts @@ -0,0 +1,102 @@ +import { + MatrixClient, + MatrixEvent, + RoomEvent, + type IContent, +} from "matrix-js-sdk"; +import { registerModuleTest } from "./base/base.js"; +import type { ICallback, ICallbackStore } from "./types.js"; +import { registerModuleAdmin } from "./admin/admin.js"; +import { registerModuleUser } from "./user/user.js"; +import { checkRoles, getUserById } from "../helpers.js"; +import { onAnyMessage, onMissingRole } from "./global.js"; +import { config } from "../config.js"; + +const callbacks: ICallbackStore = { + messageCallbacks: [], +}; + +const checkMessageCallback = ( + client: MatrixClient, + text: string, + callback: ICallback, + roomId: string, + sender: string, +) => { + if (callback.allowedRooms && !callback.allowedRooms.includes(roomId)) { + return false; + } + + if (callback.startCondition && !text.startsWith(callback.startCondition)) { + return false; + } + + if ( + callback.includesCondition && + !text.includes(callback.includesCondition) + ) { + return false; + } + + if (callback.allowedRoles && !checkRoles(callback.allowedRoles, sender)) { + onMissingRole(client, getUserById(sender).role, roomId); + return false; + } + + return true; +}; + +const registerModules = (client: MatrixClient) => { + const startupTime = Date.now(); + + client.on(RoomEvent.Timeline, (event: MatrixEvent) => { + const ts = event.getTs(); + if (ts < startupTime) { + return; + } + + if (event.getType() !== "m.room.message") { + return; + } + + const content = event.getContent(); + const body = content?.body; + if (!body || !client) { + return; + } + + const roomId = event.getRoomId(); + const sender = event.getSender(); + if (!roomId || !sender) { + return; + } + + if (sender === config.userId) { + return; + } + + console.log(`Message from ${sender} in ${roomId}: ${body}`); + + onAnyMessage(client, body.toString(), roomId, sender); + + callbacks.messageCallbacks.forEach((callback) => { + if ( + checkMessageCallback( + client, + body.toString(), + callback, + roomId, + sender, + ) + ) { + callback.callbackFunc(body.toString(), roomId, sender); + } + }); + }); + + registerModuleTest(client, callbacks); + registerModuleAdmin(client, callbacks); + registerModuleUser(client, callbacks); +}; + +export { registerModules }; diff --git a/src/modules/types.ts b/src/modules/types.ts new file mode 100644 index 0000000..c98be61 --- /dev/null +++ b/src/modules/types.ts @@ -0,0 +1,15 @@ +import type { TRole } from "../store/types.js"; + +interface ICallbackStore { + messageCallbacks: ICallback[]; +} + +interface ICallback { + startCondition?: string; + includesCondition?: string; + allowedRoles?: TRole[]; + allowedRooms?: string; + callbackFunc: (text: string, roomId: string, sender: string) => void; +} + +export { type ICallbackStore, type ICallback }; diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts new file mode 100644 index 0000000..fd3004f --- /dev/null +++ b/src/modules/user/index.ts @@ -0,0 +1 @@ +export * from "./user.js"; diff --git a/src/modules/user/user.ts b/src/modules/user/user.ts new file mode 100644 index 0000000..a03cea7 --- /dev/null +++ b/src/modules/user/user.ts @@ -0,0 +1,57 @@ +import { MatrixClient } from "matrix-js-sdk"; +import type { ICallbackStore } from "../types.js"; +import { config } from "../../config.js"; +import { getRank, getUserById, getUserName } from "../../helpers.js"; +import { state } from "../../store/store.js"; +import type { IUser } from "../../store/types.js"; +let client: MatrixClient; + +const registerModuleUser = ( + matrixClient: MatrixClient, + callbackStore: ICallbackStore, +) => { + client = matrixClient; + + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}rank`, + callbackFunc: onRank, + }); + callbackStore.messageCallbacks.push({ + startCondition: `${config.app.triggerPrefix}leaderboard`, + callbackFunc: onLeaderboard, + }); +}; + +const onRank = (_text: string, roomId: string, sender: string) => { + const rank = getRank(getUserById(sender).experience); + + client.sendHtmlMessage( + roomId, + "", + `

Your Rank: ${rank.rank}

+ Next rank progress: ${rank.experienceInRank}/${rank.expToNextRank}exp`, + ); +}; + +const onLeaderboard = (_text: string, roomId: string) => { + const mapUsersToLeaderboard = (user: IUser): string => { + const rank = getRank(user.experience); + + return `
  • ${getUserName(user)}: rank ${rank.rank} (${rank.experienceInRank}/${rank.expToNextRank}exp)
  • `; + }; + + const users = state.users.sort( + (userA, userB) => userB.experience - userA.experience, + ); + + client.sendHtmlMessage( + roomId, + "", + `

    Leaderboard

    +
      + ${users.map(mapUsersToLeaderboard)} +
    `, + ); +}; + +export { registerModuleUser }; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..6f8f47b --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,2 @@ +export * from "./store.js"; +export * from "./types.js"; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..b35dec9 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,25 @@ +import config from "../config.json" with { type: "json" }; + +import { existsSync, readFileSync, writeFileSync } from "fs"; + +import type { IState } from "./types.js"; + +let state: IState = { + users: [], +}; + +const load = () => { + if (!existsSync(config.storePath)) { + return; + } + + const json = readFileSync(config.storePath).toString(); + state = JSON.parse(json) as IState; +}; + +const save = () => { + const json = JSON.stringify(state); + writeFileSync(config.storePath, json); +}; + +export { state, load, save }; diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 0000000..5731e70 --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,14 @@ +interface IState { + users: IUser[]; +} + +interface IUser { + id: string; + role: TRole; + experience: number; + lastMessageTimestamp: number; +} + +type TRole = "NONE" | "USER" | "MODERATOR" | "ADMIN"; + +export { type IState, type IUser, type TRole }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..572c456 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,8 @@ +interface IRank { + rank: number; + experience: number; + experienceInRank: number; + expToNextRank: number; +} + +export { type IRank }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..45f5814 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,46 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": ["node"], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "resolveJsonModule": true, + "esModuleInterop": true, + }, +}