This commit is contained in:
Kiril Burlaka 2025-11-09 09:28:51 +01:00
commit 635af85b2b
16 changed files with 4550 additions and 0 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
/node_modules
/dist
.env

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
node_modules
.idea

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
# Declare build-time argument
ARG DOCKER_IMAGE_SOURCE
FROM ${DOCKER_IMAGE_SOURCE}
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE ${PORT}
CMD npm run build;npm run start
# CMD [ "npm", "run", "start" ]

38
docker-compose.yml Normal file
View File

@ -0,0 +1,38 @@
services:
main:
build:
dockerfile: Dockerfile
context: .
args:
DOCKER_IMAGE_SOURCE: ${DOCKER_IMAGE_SOURCE}
image: ${IMAGE_NAME}
container_name: ${CONTAINER_NAME}
depends_on:
- redis
ports:
- ${PORT}:8000
tmpfs:
- /tmp:size=${TMPFS_SIZE}
- /var/log:size=50m
deploy:
resources:
limits:
cpus: ${CPU_LIMIT}
memory: ${MEMORY_LIMIT}
volumes:
- .:/wrkdir
restart: unless-stopped
env_file: .env
redis:
image: ${REDIS_IMAGE_SOURCE}
container_name: ${REDIS_CONTAINER_NAME}
command: sh -c "redis-server --requirepass $REDIS_PASSWORD"
# should work by internal docker network
# ports:
# - "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
redis_data:

17
env.template Normal file
View File

@ -0,0 +1,17 @@
PORT=8000
CONTAINER_NAME='cc'
IMAGE_NAME='ii'
TMPFS_SIZE='8192m'
CPU_LIMIT='10'
MEMORY_LIMIT='8g'
DOCKER_IMAGE_SOURCE='node:20.12.1-alpine'
#DOCKER_IMAGE_SOURCE="harbor.lesobirzha.ru:8008/library/node:20.12.1-alpine"
NODE_ENV='LOCAL'
TELEGRAM_TOKEN='secret_token'
REDIS_HOST='redis'
REDIS_PORT=6379
REDIS_PASSWORD='gdgkjdgjapoie29u)(@9ghieihf2)'
REDIS_CONTAINER_NAME='streamtreebot-redis'
REDIS_IMAGE_SOURCE='redis:alpine'
#REDIS_IMAGE_SOURCE='harbor.lesobirzha.ru:8008/library/redis:alpine'
DEVCHATID='123456789'

31
eslint.config.cjs Normal file
View File

@ -0,0 +1,31 @@
// eslint.config.cjs
import js from "@eslint/js";
import ts from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import globals from "globals";
module.exports = [
js.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.node,
},
},
plugins: {
"@typescript-eslint": ts,
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
indent: "off",
"linebreak-style": "off",
quotes: "off",
semi: ["error", "always"],
},
},
];

4150
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "streamtree",
"version": "1.0.0",
"type": "commonjs",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"start": "node build/index.js",
"lint": "npx eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"author": "Kiril Burlaka",
"license": "ISC",
"dependencies": {
"@eslint/js": "^9.35.0",
"axios": "^1.11.0",
"dotenv": "^17.2.2",
"globals": "^16.3.0",
"node-telegram-bot-api": "^0.66.0",
"redis": "^5.8.2"
},
"devDependencies": {
"@types/node": "^24.3.1",
"@types/node-telegram-bot-api": "^0.64.10",
"@typescript-eslint/eslint-plugin": "^8.42.0",
"@typescript-eslint/parser": "^8.42.0",
"eslint": "^9.35.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
}
}

59
src/config.ts Normal file
View File

@ -0,0 +1,59 @@
import * as process from "node:process";
import {NODE_ENVS} from "./typesRelated/enums";
class Config {
public readonly botToken: string;
public readonly redisHost: string;
public readonly redisPort: number;
public readonly redisPassword: string;
public readonly nodeEnv: NODE_ENVS;
public readonly developersChatId: number;
constructor() {
const {
TELEGRAM_TOKEN,
REDIS_HOST,
REDIS_PORT,
REDIS_PASSWORD,
NODE_ENV,
DEVCHATID,
} = process.env;
if (!TELEGRAM_TOKEN) {
throw new Error('Config constructor no TELEGRAM_TOKEN')
}
this.botToken = TELEGRAM_TOKEN;
if (
!REDIS_HOST ||
isNaN(+REDIS_PORT) ||
!REDIS_PASSWORD
) {
throw new Error('Config constructor no REDIS_HOST || REDIS_PORT || REDIS_PASSWORD')
}
this.redisHost = REDIS_HOST
this.redisPort = +REDIS_PORT
this.redisPassword = REDIS_PASSWORD
if (
!NODE_ENV ||
!Object.values(NODE_ENVS).some(
el => el === NODE_ENV
)
) {
throw new Error('invalid NODE_ENV');
}
this.nodeEnv = NODE_ENV as NODE_ENVS;
if (!DEVCHATID && NODE_ENV !== NODE_ENVS.PROD) {
throw new Error('no DEVCHATID')
}
this.developersChatId = +DEVCHATID;
console.log('Config is ready');
}
}
export default new Config();

16
src/index.ts Normal file
View File

@ -0,0 +1,16 @@
import TelegramBot from 'node-telegram-bot-api'
import Config from "./config";
import BotUtils from "./utils/bot.utils";
import RedisUtils from "./utils/redis.utils";
async function main() {
await RedisUtils.init();
const BOT_TOKEN = Config.botToken;
const bot = new TelegramBot(BOT_TOKEN, { polling: true });
BotUtils.registerPaths(bot);
console.log('index.js started')
}
main();

View File

@ -0,0 +1,5 @@
export enum NODE_ENVS {
LOCAL = 'LOCAL',
DEV = "DEV",
PROD = "PROD",
}

View File

80
src/utils/bot.utils.ts Normal file
View File

@ -0,0 +1,80 @@
import Config from "../config.js";
import RedisUtils from "./redis.utils";
import TelegramBot, { Message } from "node-telegram-bot-api";
class BotUtils {
private bot: TelegramBot;
registerPaths(bot: TelegramBot) {
this.bot = bot;
// bot.onText(/\/start_stream/, this.startStream.bind(this));
// bot.onText(/\/stop_stream/, this.stopStream.bind(this));
// bot.onText(/\/status/, this.getStatus.bind(this));
//
// const botCommands = [
// { command: "/start_stream", description: "Start pod" },
// { command: "/stop_stream", description: "Stop pod" },
// { command: "/status", description: "Session status" }
// ];
//
// if (Config.nodeEnv === NODE_ENVS.LOCAL || Config.nodeEnv === NODE_ENVS.DEV) {
// botCommands.push(...this.registerDevPaths(bot));
// }
// bot.setMyCommands(botCommands);
}
//dev
// registerDevPaths(bot: TelegramBot): TelegramBot.BotCommand[] {
// bot.onText(
// /\/clearsession/,
// this.devPathHandle.bind(
// this,
// this.clearSession.bind(this),
// '/clearsession'
// )
// );
// bot.onText(
// /\/session/,
// this.devPathHandle.bind(
// this,
// this.getSession.bind(this),
// '/session'
// )
// );
// bot.onText(
// /\/podinfo/,
// this.devPathHandle.bind(
// this,
// this.getPodInfo.bind(this),
// '/podinfo'
// )
// );
// bot.onText(
// /\/dockerimage (.+)/,
// this.devPathHandle.bind(
// this,
// this.setDockerImage.bind(this),
// '/dockerimage (.+)'
// )
// );
// bot.onText(
// /\/listdockerimages/,
// this.devPathHandle.bind(
// this,
// this.listDockerImages.bind(this),
// '/listdockerimages'
// )
// );
//
// return [
// { command: "/clearsession", description: "dev clear session" },
// { command: "/session", description: "dev print session" },
// { command: "/podinfo", description: "dev pod info" },
// { command: "/dockerimage", description: "dev set dockerimage ending" },
// { command: "/listdockerimages", description: "dev list some of known values for /dockerimage" },
// ];
// }
}
export default new BotUtils();

View File

@ -0,0 +1,2 @@
export const waitXSeconds = (s: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, s * 1000));

73
src/utils/redis.utils.ts Normal file
View File

@ -0,0 +1,73 @@
import {createClient, RedisClientType} from 'redis';
import Config from "../config";
import {UserSession} from "../typesRelated/types";
class RedisUtils {
private readonly client: RedisClientType;
private readonly podIdToChatIdKey: string;
constructor() {
this.client = createClient({
socket: {
host: Config.redisHost,
port: Config.redisPort
},
password: Config.redisPassword
});
this.client.connect().catch(console.error);
this.podIdToChatIdKey = 'pod_id_to_chat_id';
}
async init() {
await this.defineVariables();
}
async setValue(key: string, value: any) {
if (['string', 'number'].includes(typeof value)) {
await this.client.set(key, value);
} else {
await this.client.set(key, JSON.stringify(value));
}
}
async getValue(key: string): Promise<any> {
const value = await this.client.get(key);
if (typeof value === 'object') {
return value;
}
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
async deleteValue(key: string) {
return this.client.del(key);
}
async defineVariables() {
// const pIdToCId = this.getValue(this.podIdToChatIdKey);
// if (!pIdToCId || typeof pIdToCId !== 'object') {
// await this.setValue(this.podIdToChatIdKey, {});
// }
}
// podIdToChatId(podId: string) {
// const pIdToCId = this.getValue(this.podIdToChatIdKey);
// return pIdToCId[podId];
// }
saveSession(userId: number, session: UserSession) {
return this.setValue(`user:${userId}`, session);
}
getSession(userId: number): Promise<UserSession> {
return this.getValue(`user:${userId}`);
}
deleteSession(userId: number) {
return this.deleteValue(`user:${userId}`);
}
}
export default new RedisUtils();

24
tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es2022",
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"removeComments": true,
"rootDir": "src",
"outDir": "build",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"declaration": true,
"incremental": true,
"skipLibCheck": true,
},
}