Initial commit: Tapalka monorepo (bot, front, strapi)
This commit is contained in:
15
CMyTapper/robucks-bot/.env
Normal file
15
CMyTapper/robucks-bot/.env
Normal 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
|
||||
|
||||
24
CMyTapper/robucks-bot/.eslintrc
Normal file
24
CMyTapper/robucks-bot/.eslintrc
Normal 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"
|
||||
}
|
||||
}
|
||||
27
CMyTapper/robucks-bot/.github/workflows/prod.yaml
vendored
Normal file
27
CMyTapper/robucks-bot/.github/workflows/prod.yaml
vendored
Normal 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
3
CMyTapper/robucks-bot/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
/node_modules
|
||||
/build
|
||||
10
CMyTapper/robucks-bot/ecosystem.config.js
Normal file
10
CMyTapper/robucks-bot/ecosystem.config.js
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
6
CMyTapper/robucks-bot/nodemon.json
Normal file
6
CMyTapper/robucks-bot/nodemon.json
Normal 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
3903
CMyTapper/robucks-bot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
CMyTapper/robucks-bot/package.json
Normal file
40
CMyTapper/robucks-bot/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
63
CMyTapper/robucks-bot/src/api/players.api.ts
Normal file
63
CMyTapper/robucks-bot/src/api/players.api.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
106
CMyTapper/robucks-bot/src/bot.ts
Normal file
106
CMyTapper/robucks-bot/src/bot.ts
Normal 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;
|
||||
122
CMyTapper/robucks-bot/src/index.ts
Normal file
122
CMyTapper/robucks-bot/src/index.ts
Normal 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();
|
||||
});
|
||||
5
CMyTapper/robucks-bot/src/types/express.type.ts
Normal file
5
CMyTapper/robucks-bot/src/types/express.type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export interface TypedRequestBody<T> extends Request {
|
||||
body: T;
|
||||
}
|
||||
14
CMyTapper/robucks-bot/src/utils/GetDifferenceInDays.ts
Normal file
14
CMyTapper/robucks-bot/src/utils/GetDifferenceInDays.ts
Normal 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;
|
||||
}
|
||||
48
CMyTapper/robucks-bot/src/utils/HttpsInstanceFactory.ts
Normal file
48
CMyTapper/robucks-bot/src/utils/HttpsInstanceFactory.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
6
CMyTapper/robucks-bot/src/utils/encrypter.ts
Normal file
6
CMyTapper/robucks-bot/src/utils/encrypter.ts
Normal 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();
|
||||
};
|
||||
14
CMyTapper/robucks-bot/tsconfig.json
Normal file
14
CMyTapper/robucks-bot/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
2080
CMyTapper/robucks-bot/yarn.lock
Normal file
2080
CMyTapper/robucks-bot/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user