Initial commit: Tapalka monorepo (bot, front, strapi)

This commit is contained in:
2026-05-21 11:54:44 +07:00
commit 9ec9485940
208 changed files with 58314 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
STRAPI_USER="admin"
STRAPI_PASSWORD="Admin1234"
CRYPTO_SECRET=abcDef$
PORT=3001
# PROD
#WEBAPP_URL="https://rbx-click.ru"
#STRAPI_URL="https://strapi.rbx-click.ru/api/"
#STRAPI_URL_BASE=https://strapi.rbx-click.ru
# LOCAL
WEBAPP_URL=https://runtime-monster-journal-logic.trycloudflare.com
STRAPI_URL="http://127.0.0.1:1337/api/"
STRAPI_URL_BASE=http://127.0.0.1:1337

View File

@@ -0,0 +1,24 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": ["warn", {
"endOfLine": "auto",
"semi": true,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all",
"useTabs": false,
"tabWidth": 4,
"jsxSingleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid"
}
],
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "off",
"prefer-const": "warn",
"no-console": "warn"
}
}

View File

@@ -0,0 +1,27 @@
name: Deploy to prod
on:
workflow_dispatch:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy with pm2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{secrets.PROD_SSH_HOST}}
username: ${{secrets.PROD_SSH_USERNAME}}
password: ${{secrets.PROD_SSH_PASSWORD}}
script_stop: true
script: |
export NVM_DIR=~/.nvm
source ~/.nvm/nvm.sh
cd ${{secrets.PROD_PROJECT_PATH}}
git checkout main
git pull
yarn install
pm2 restart ecosystem.config.js

3
CMyTapper/robucks-bot/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
/node_modules
/build

View File

@@ -0,0 +1,10 @@
module.exports = {
apps: [
{
name: 'bot',
script: 'npm run start',
time: true,
log_date_format: 'DD.MM.YYYY HH:mm:ss',
},
],
};

View File

@@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "npx ts-node ./src/index.ts"
}

3903
CMyTapper/robucks-bot/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
{
"name": "clicker-bot",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "npx nodemon",
"start": "npx ts-node src/index.ts"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@types/axios": "^0.14.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^7.14.1",
"axios": "^1.7.2",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5",
"eslint": "8",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"express": "^4.19.2",
"prettier": "^3.3.3",
"telegraf": "^4.16.3",
"typescript": "^5.5.2"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/crypto-js": "^4.2.2",
"@types/node": "^20.14.9",
"nodemon": "^3.1.4",
"rimraf": "^5.0.7",
"ts-node": "^10.9.2"
}
}

View File

@@ -0,0 +1,63 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '../utils/HttpsInstanceFactory';
export class PlayersApi {
private static instance: PlayersApi;
private httpInstance: AxiosInstance;
private populateQuery = 'populate=referral&populate=my_referrals';
constructor() {
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance();
}
public static getInstance() {
if (!this.instance) {
this.instance = new PlayersApi();
}
return this.instance;
}
public async connectReferrals(referralId: number, invitedId: number) {
await this.updatePlayer(referralId, {
data: {
my_referrals: {
connect: [invitedId],
},
},
});
await this.updatePlayer(invitedId, {
data: {
referral: {
set: [referralId],
},
},
});
}
public async updatePlayer(id: number | string, data: any) {
return (await this.httpInstance.put(`players/${id}?${this.populateQuery}`, data)).data.data;
}
public async createPlayer(data: any) {
const createdPlayer = (await this.httpInstance.post(`players?${this.populateQuery}`, data))
.data.data;
await this.httpInstance.post(`/archive-requests`, {
data: {
player: {
connect: [createdPlayer.id],
},
},
});
return createdPlayer;
}
public async getPlayerByTelegramId(id: number | string) {
const filteredPlayers = (
await this.httpInstance.get(`players?filters[telegram_id]=${id}&${this.populateQuery}`)
).data.data as any[];
if (filteredPlayers.length > 0) {
return filteredPlayers[0];
}
return null;
}
}

View File

@@ -0,0 +1,106 @@
import { Markup, Telegraf } from 'telegraf';
import { PlayersApi } from './api/players.api';
import { encryptId } from './utils/encrypter';
import { NotifyParams } from './index';
class Bot {
private botInstance: Telegraf;
constructor(token: string) {
this.botInstance = new Telegraf(token);
}
public async launch() {
process.once('SIGINT', () => this.botInstance.stop('SIGINT'));
process.once('SIGTERM', () => this.botInstance.stop('SIGTERM'));
await this.botInstance.launch();
}
public start() {
this.botInstance.start(async ctx => {
const playersApi = PlayersApi.getInstance();
try {
const user_id = String(ctx.from.id);
const username = ctx.from.username || ctx.from.first_name;
const encrypted_id = encryptId(user_id);
let me = await playersApi.getPlayerByTelegramId(user_id);
if (!me) {
me = await playersApi.createPlayer({
data: {
telegram_id: user_id,
telegram_nick: username,
balance: 0,
old_balance: 0,
},
});
}
await playersApi.updatePlayer(me.id, {
data: {
auth_id: encrypted_id,
telegram_nick: username,
},
});
await ctx.reply(
'Привет! Нажми на кнопку ниже, чтобы открыть веб-приложение.',
Markup.inlineKeyboard([
[
Markup.button.webApp(
'Открыть WebApp',
`${process.env.WEBAPP_URL}/${encodeURIComponent(encrypted_id)}/home` ||
'',
),
],
]),
);
const args = ctx.message.text.split(' ');
let referral_user_id = null;
if (args.length > 1) {
referral_user_id = +args[1].substring(4);
}
const sender = await playersApi.getPlayerByTelegramId(referral_user_id || -1)
if (
!sender ||
sender.id === me.id ||
!!me.attributes.referral.data ||
me.attributes.my_referrals.data.some(
(referral: { id: number }) => referral.id === sender.id,
)
) {
return;
}
await playersApi.connectReferrals(sender.id, me.id);
} catch (err) {
console.log('ERROR: /start', err);
}
});
}
public notifyByNewExchange({ exchange_id, telegram_id }: NotifyParams) {
const requestLink = `${process.env.STRAPI_URL_BASE}/admin/content-manager/collection-types/api::exchange-request.exchange-request/${exchange_id}`;
const msgTxt = `Поступила новая <a href="${requestLink}">заявка</a> на обмен!`;
for (const id of telegram_id) {
this.botInstance.telegram
.sendMessage(id, msgTxt, {
parse_mode: 'HTML',
})
.catch(err => console.log('ERROR: notify on new exchange', err));
}
}
public getBotLink() {
return `https://t.me/${this.botInstance.botInfo?.username}?start=`;
}
public stop() {
this.botInstance.stop();
}
}
export default Bot;

View File

@@ -0,0 +1,122 @@
import express, { Response } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import Bot from './bot';
import { HttpInstanceFactory } from './utils/HttpsInstanceFactory';
import { TypedRequestBody } from './types/express.type';
import { getDifferenceInDays } from './utils/GetDifferenceInDays';
dotenv.config({ path: path.resolve(__dirname, '../.env') });
export type NotifyParams = {
telegram_id: string[] | number[];
exchange_id: string | number;
};
export let bot: Bot;
const app = express();
const PORT = process.env.PORT as string;
const restartTimeout = 10;
app.use(express.json());
app.use(cors());
app.post('/notify', async (req: TypedRequestBody<NotifyParams>, res: Response) => {
bot.notifyByNewExchange(req.body);
return res.status(200).send({});
});
app.get('/link-bot', async (req, res) => {
const linkBot = bot.getBotLink();
return res.status(200).send({ link: linkBot });
});
app.get('/config-bot-data', async (req, res) => {
HttpInstanceFactory.getAuthorizedInstance()
.get('/config')
.then(async response => {
const { token, dateLifetimeJwt } = response.data.data.attributes;
if (!token || getDifferenceInDays(new Date(), dateLifetimeJwt) >= 30) {
const baseInstance = HttpInstanceFactory.getBaseInstance();
const dateLifeTime = new Date();
const user = await baseInstance.post(`auth/local`, {
identifier: process.env.STRAPI_USER ?? '',
password: process.env.STRAPI_PASSWORD ?? '',
});
await HttpInstanceFactory.getAuthorizedInstance().put(`config`, {
data: {
token: user.data.jwt,
dateLifetimeJwt: dateLifeTime,
},
});
return res.status(200).send({
token: user.data.jwt,
dateLifetimeJwt: dateLifeTime,
});
} else {
return res.status(200).send({
token: token,
dateLifetimeJwt: dateLifetimeJwt,
});
}
});
});
app.post('/restart-bot', async (req, res) => {
if (bot) {
try {
await bot.stop();
console.log('Bot stopped successfully.');
} catch (error) {
console.log('Error stopping the bot:', error);
}
}
launchBot();
return res.status(200).send({ message: 'Bot is restarting...' });
});
const launchBot = async () => {
console.log(`Starting on port ${PORT}...`);
HttpInstanceFactory.updateAuthorizedInstance()
.then(() => {
HttpInstanceFactory.getAuthorizedInstance()
.get('bot-config')
.then(res => {
bot = new Bot(res.data.data.attributes.token);
bot.launch().catch(() => {
console.log(`ERROR: launching bot. Restarting in ${restartTimeout} sec.`);
setTimeout(() => {
launchBot();
}, restartTimeout * 1000);
});
bot.start();
})
.catch(() => {
console.log(`ERROR: getting bot config. Restarting in ${restartTimeout} sec.`);
setTimeout(() => {
launchBot();
}, restartTimeout * 1000);
});
})
.catch(() => {
console.log(`ERROR: authorization. Restarting in ${restartTimeout} sec.`);
setTimeout(() => {
launchBot();
}, restartTimeout * 1000);
});
};
app.listen(PORT, () => {
launchBot();
});

View File

@@ -0,0 +1,5 @@
import { Request } from 'express';
export interface TypedRequestBody<T> extends Request {
body: T;
}

View File

@@ -0,0 +1,14 @@
export function getDifferenceInDays(currentDate: Date, dateLifeTimeJwt: string | undefined) {
if (!dateLifeTimeJwt) {
return 31;
}
const dateLifeTime = new Date(dateLifeTimeJwt);
const oneDay = 24 * 60 * 60 * 1000;
const diffInTime = dateLifeTime.getTime() - currentDate.getTime();
const diffInDays = Math.round(Math.abs(diffInTime) / oneDay);
return diffInDays;
}

View File

@@ -0,0 +1,48 @@
import axios, { AxiosInstance } from 'axios';
import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const BASE_URL = process.env.STRAPI_URL;
export class HttpInstanceFactory {
private static baseInstance: AxiosInstance | null = null;
private static authorizedInstance: AxiosInstance | null = null;
public static getBaseInstance(): AxiosInstance {
if (this.baseInstance) return this.baseInstance;
this.baseInstance = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
return this.baseInstance;
}
public static getAuthorizedInstance(token?: string): AxiosInstance {
if ((!this.authorizedInstance && token) || token) {
this.authorizedInstance = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
}
if (this.authorizedInstance) {
return this.authorizedInstance;
}
return this.getBaseInstance();
}
public static async updateAuthorizedInstance() {
const res = await this.getBaseInstance().post('auth/local', {
identifier: process.env.STRAPI_USER ?? '',
password: process.env.STRAPI_PASSWORD ?? '',
});
HttpInstanceFactory.getAuthorizedInstance(res.data.jwt);
}
}

View File

@@ -0,0 +1,6 @@
import crypto from 'crypto-js';
export const encryptId = (id: string): string => {
const secret = String(process.env.CRYPTO_SECRET);
return crypto.AES.encrypt(id, secret).toString();
};

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["es6"],
"allowJs": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}

File diff suppressed because it is too large Load Diff