Mohammed Foud commited on
Commit
80d4bc1
·
1 Parent(s): 308b897
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .commitlintrc.json +3 -0
  2. .cursorignore +4 -0
  3. .dockerignore +7 -0
  4. .editorconfig +11 -0
  5. .eslintignore +2 -0
  6. .eslintrc.cjs +4 -0
  7. .gitignore +40 -0
  8. .npmrc +1 -0
  9. .vscode/extensions.json +3 -0
  10. .vscode/settings.json +21 -0
  11. Dockerfile +13 -19
  12. a.py +13 -0
  13. d.sh +4 -0
  14. db.sql +185 -0
  15. mousa-backend-main.zip +0 -3
  16. package.json +54 -0
  17. pnpm-lock.yaml +0 -0
  18. run.sh +4 -0
  19. src/api/paymentWebhooks.ts +47 -0
  20. src/bots/botManager.ts +173 -0
  21. src/bots/db.sql +110 -0
  22. src/bots/handlers/balanceHandlers.ts +283 -0
  23. src/bots/handlers/commandHandlers.ts +150 -0
  24. src/bots/handlers/giftHandlers.ts +175 -0
  25. src/bots/handlers/historyHandlers.ts +155 -0
  26. src/bots/handlers/index.ts +44 -0
  27. src/bots/handlers/languageHandlers.ts +74 -0
  28. src/bots/handlers/mainMenuHandlers.ts +145 -0
  29. src/bots/handlers/paymentWebhookHandlers.ts +47 -0
  30. src/bots/handlers/profileHandlers.ts +318 -0
  31. src/bots/handlers/purchaseHandlers.ts +232 -0
  32. src/bots/handlers/serviceHandlers.ts +174 -0
  33. src/bots/index.ts +100 -0
  34. src/bots/services/AdminService.ts +24 -0
  35. src/bots/services/CryptoService.ts +43 -0
  36. src/bots/services/PayPalService.ts +133 -0
  37. src/bots/services/PaymentVerificationService.ts +83 -0
  38. src/bots/services/PurchaseTrackingService.ts +174 -0
  39. src/bots/services/VirtualNumberService.ts +123 -0
  40. src/bots/services/auth.ts +179 -0
  41. src/bots/types/botTypes.ts +17 -0
  42. src/bots/types/paymentTypes.ts +11 -0
  43. src/bots/utils/country.ts +171 -0
  44. src/bots/utils/handlerUtils.ts +64 -0
  45. src/bots/utils/keyboardUtils.ts +247 -0
  46. src/bots/utils/messageManager.ts +99 -0
  47. src/bots/utils/messageUtils.ts +160 -0
  48. src/bots/utils/priceUtils.ts +128 -0
  49. src/bots/utils/settings.ts +96 -0
  50. src/config.ts +54 -0
.commitlintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": ["@commitlint/config-conventional"]
3
+ }
.cursorignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules
2
+ trash
3
+ build
4
+ pnpm-lock.yaml
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ **/node_modules
2
+ */node_modules
3
+ node_modules
4
+ Dockerfile
5
+ .*
6
+ */.*
7
+ !.env
.editorconfig ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editor configuration, see http://editorconfig.org
2
+
3
+ root = true
4
+
5
+ [*]
6
+ charset = utf-8
7
+ indent_style = tab
8
+ indent_size = 2
9
+ end_of_line = lf
10
+ trim_trailing_whitespace = true
11
+ insert_final_newline = true
.eslintignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ docker-compose
2
+ kubernetes
.eslintrc.cjs ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['@antfu'],
4
+ }
.gitignore ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .firebase
3
+ .firebaserc
4
+ # Logs
5
+ logs
6
+ *.log
7
+ npm-debug.log*
8
+ yarn-debug.log*
9
+ yarn-error.log*
10
+ pnpm-debug.log*
11
+ lerna-debug.log*
12
+
13
+ node_modules
14
+ .DS_Store
15
+ # dist
16
+ turch
17
+ dist-ssr
18
+ coverage
19
+ *.local
20
+
21
+ /cypress/videos/
22
+ /cypress/screenshots/
23
+
24
+ # Editor directories and files
25
+ .vscode/*
26
+ !.vscode/settings.json
27
+ !.vscode/extensions.json
28
+ .idea
29
+ *.suo
30
+ *.ntvs*
31
+ *.njsproj
32
+ *.sln
33
+ *.sw?
34
+
35
+ # Environment variables files
36
+ /service/.env
37
+ trash
38
+ logs
39
+ .env
40
+ .git2222
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ strict-peer-dependencies=false
.vscode/extensions.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "recommendations": ["dbaeumer.vscode-eslint"]
3
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "prettier.enable": false,
3
+ "editor.formatOnSave": false,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.eslint": "explicit"
6
+ },
7
+ "eslint.validate": [
8
+ "javascript",
9
+ "typescript",
10
+ "json",
11
+ "jsonc",
12
+ "json5",
13
+ "yaml"
14
+ ],
15
+ "cSpell.words": [
16
+ "antfu",
17
+
18
+ "esno",
19
+
20
+ ]
21
+ }
Dockerfile CHANGED
@@ -1,32 +1,26 @@
 
1
  FROM node:22.13.1-slim
2
 
3
- # Install unzip
4
- RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
5
-
6
- # Set working directory
7
  WORKDIR /usr/src/app
8
 
9
- # Copy and unzip the zip file
10
- COPY mousa-backend-main.zip ./
11
- RUN unzip mousa-backend-main.zip && rm mousa-backend-main.zip
12
-
13
- # Change to the extracted folder
14
- WORKDIR /usr/src/app/mousa-backend-main
15
-
16
  # Install pnpm globally
17
  RUN npm install -g pnpm
18
 
19
- # Install dependencies
 
 
 
20
  RUN pnpm install --frozen-lockfile
21
 
22
- # Build the app
23
- RUN pnpm run build
24
 
25
- # Create writable logs directory
26
- RUN mkdir -p logs && chmod 777 logs
27
 
28
- # Expose the app port
29
  EXPOSE 7860
30
 
31
- # Start the app
32
- CMD ["pnpm", "run", "start"]
 
1
+ # Use the official Node.js image with the desired version
2
  FROM node:22.13.1-slim
3
 
4
+ # Set the working directory inside the container
 
 
 
5
  WORKDIR /usr/src/app
6
 
 
 
 
 
 
 
 
7
  # Install pnpm globally
8
  RUN npm install -g pnpm
9
 
10
+ # Copy package.json and pnpm-lock.yaml
11
+ COPY package.json pnpm-lock.yaml* ./
12
+
13
+ # Install dependencies using pnpm
14
  RUN pnpm install --frozen-lockfile
15
 
16
+ # Copy the rest of the application code
17
+ COPY . .
18
 
19
+ # Build the TypeScript project
20
+ RUN pnpm run build
21
 
22
+ # Expose the port your app runs on
23
  EXPOSE 7860
24
 
25
+ # Start the application
26
+ CMD ["pnpm", "run", "start"]
a.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from fivesimapi import fivesim
3
+
4
+ api_key = "YOUR_API_KEY" # Replace with your actual API key
5
+
6
+ async def main():
7
+ client = fivesim.FiveSim(api_key)
8
+ country = "any" # You can specify a country code like 'us', 'ru', etc.
9
+ products = await client.get_products(country)
10
+ for product in products:
11
+ print(f"Product: {product['product']}, Price: {product['cost']}, Available: {product['count']}")
12
+
13
+ asyncio.run(main())
d.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ git add .
2
+ git commit -m "allh"
3
+ git push
4
+
db.sql ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ INSERT INTO
2
+ "public"."blockkeyword" ("text", "created_at", "user_id")
3
+ VALUES
4
+ (
5
+ 'https://wa.me',
6
+ '2024-02-28 21:04:51.610793+00',
7
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
8
+ ),
9
+ (
10
+ 'أنصحكم فيها صراحة',
11
+ '2024-02-28 21:04:51.610793+00',
12
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
13
+ ),
14
+ (
15
+ 'اتصل بنا الآن للحصول',
16
+ '2024-02-28 21:04:51.610793+00',
17
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
18
+ ),
19
+ (
20
+ 'اضغط المشبك',
21
+ '2024-02-28 21:04:51.610793+00',
22
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
23
+ ),
24
+ (
25
+ 'اعداد البحوث باللغتين',
26
+ '2024-02-28 21:04:51.610793+00',
27
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
28
+ ),
29
+ (
30
+ 'اعطيك احد يحل فل مارك',
31
+ '2024-02-19 19:10:29.482826+00',
32
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
33
+ ),
34
+ (
35
+ 'الدفع بعد',
36
+ '2024-02-15 16:12:20.500052+00',
37
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
38
+ ),
39
+ (
40
+ 'انا مصمم فلسطنيي',
41
+ '2024-02-28 21:04:51.610793+00',
42
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
43
+ ),
44
+ (
45
+ 'بدون فلوس',
46
+ '2024-02-27 19:22:19.13035+00',
47
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
48
+ ),
49
+ (
50
+ 'بدون مقابل',
51
+ '2024-02-14 17:03:30.505531+00',
52
+ 'dcb2d2ae-4039-4172-a347-7d324337eca8'
53
+ ),
54
+ (
55
+ 'بلوشي',
56
+ '2024-02-28 21:04:51.610793+00',
57
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
58
+ ),
59
+ (
60
+ 'تواصل الآن على الوات.ساب',
61
+ '2024-02-28 21:04:51.610793+00',
62
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
63
+ ),
64
+ (
65
+ 'تواصل على الواتساب',
66
+ '2024-02-28 21:04:51.610793+00',
67
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
68
+ ),
69
+ (
70
+ 'خدماتنا مضمونة',
71
+ '2024-02-28 21:04:51.610793+00',
72
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
73
+ ),
74
+ (
75
+ 'دون مقابل مادي',
76
+ '2024-02-28 21:03:45.321797+00',
77
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
78
+ ),
79
+ (
80
+ 'سلام ذي مختصة تحل واجبات',
81
+ '2024-02-28 21:04:51.610793+00',
82
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
83
+ ),
84
+ (
85
+ 'ضمان الفل مارك',
86
+ '2024-02-28 21:04:51.610793+00',
87
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
88
+ ),
89
+ (
90
+ 'عمل سيره ذاتية',
91
+ '2024-02-28 21:04:51.610793+00',
92
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
93
+ ),
94
+ (
95
+ 'لتواصل عبر الواتساب',
96
+ '2024-02-28 21:04:51.610793+00',
97
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
98
+ ),
99
+ (
100
+ 'لجميع الخدمات الطلابية',
101
+ '2024-02-28 21:04:51.610793+00',
102
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
103
+ ),
104
+ (
105
+ 'لطلب الخدمة',
106
+ '2024-02-28 21:04:51.610793+00',
107
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
108
+ ),
109
+ (
110
+ 'لطلب المساعده',
111
+ '2024-02-28 21:04:51.610793+00',
112
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
113
+ ),
114
+ (
115
+ 'للتواصل',
116
+ '2024-02-28 21:04:51.610793+00',
117
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
118
+ ),
119
+ (
120
+ 'للتواصل أو الاستفسار عبر واتساب',
121
+ '2024-02-28 21:04:51.610793+00',
122
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
123
+ ),
124
+ (
125
+ 'لو احد محتاج دكتور',
126
+ '2024-02-28 21:04:51.610793+00',
127
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
128
+ ),
129
+ (
130
+ 'مجان ',
131
+ '2024-02-28 21:04:43.213204+00',
132
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
133
+ ),
134
+ (
135
+ '(منصة الأمتياز)',
136
+ '2024-02-28 21:04:51.610793+00',
137
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
138
+ ),
139
+ (
140
+ 'مواعيد اختبار التحصيلي',
141
+ '2024-02-28 21:04:51.610793+00',
142
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
143
+ ),
144
+ (
145
+ 'هذا رقمه',
146
+ '2024-02-28 21:04:51.610793+00',
147
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
148
+ ),
149
+ (
150
+ 'هذي دكتوره تساعد وحلها كويس مجربه',
151
+ '2024-02-28 21:04:51.610793+00',
152
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
153
+ ),
154
+ (
155
+ 'يتواصل ع الرقم',
156
+ '2024-02-28 21:04:51.610793+00',
157
+ '7ae51109-17d1-4c42-aa60-b222a36fc76a'
158
+ );
159
+
160
+
161
+ -- Command table
162
+ CREATE TABLE command (
163
+ id SERIAL PRIMARY KEY,
164
+ key VARCHAR(50) NOT NULL UNIQUE,
165
+ value VARCHAR(255) NOT NULL,
166
+ description TEXT,
167
+ is_active BOOLEAN DEFAULT TRUE,
168
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
169
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
170
+ );
171
+
172
+ -- Message table (linked to command via one-to-many relationship)
173
+ CREATE TABLE message (
174
+ id SERIAL PRIMARY KEY,
175
+ command_id INTEGER REFERENCES command(id) ON DELETE CASCADE,
176
+ key VARCHAR(50) NOT NULL,
177
+ value TEXT NOT NULL,
178
+ description TEXT,
179
+ is_active BOOLEAN DEFAULT TRUE,
180
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
181
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
182
+ );
183
+
184
+ -- Add unique constraint for command_id + key combination
185
+ CREATE UNIQUE INDEX message_command_key_unique ON message(command_id, key);
mousa-backend-main.zip DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:28273693a30c40a730d3c546792211e5fe01fcab9be89fd07e4158d98e67d10e
3
- size 146326
 
 
 
 
package.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "telegram-dash",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "esno ./src/index.ts",
8
+ "dev": "esno watch ./src/index.ts",
9
+ "prod": "node ./build/index.mjs",
10
+ "build": "pnpm clean && tsup",
11
+ "clean": "rimraf build",
12
+ "lint": "eslint .",
13
+ "lint:fix": "eslint . --fix",
14
+ "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "dependencies": {
20
+ "@supabase/supabase-js": "^2.48.1",
21
+ "axios": "^1.3.4",
22
+ "big-integer": "^1.6.52",
23
+ "chalk": "^5.4.1",
24
+ "chatgpt": "^5.1.2",
25
+ "cors": "^2.8.5",
26
+ "dotenv": "^16.4.7",
27
+ "esno": "^4.7.0",
28
+ "express": "^4.21.2",
29
+ "express-rate-limit": "^6.7.0",
30
+ "https-proxy-agent": "^5.0.1",
31
+ "isomorphic-fetch": "^3.0.0",
32
+ "node-fetch": "^3.3.0",
33
+ "punycode": "^2.3.1",
34
+ "socks-proxy-agent": "^7.0.0",
35
+ "telegraf": "^4.16.3",
36
+ "telegram": "^2.26.16",
37
+ "uuid": "^11.1.0",
38
+ "winston": "^3.17.0",
39
+ "winston-daily-rotate-file": "^5.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@antfu/eslint-config": "^0.35.3",
43
+ "@types/cors": "^2.8.17",
44
+ "@types/express": "^5.0.0",
45
+ "@types/node": "^22.13.0",
46
+ "eslint": "^8.35.0",
47
+ "rimraf": "^4.3.0",
48
+ "ts-node": "^10.9.2",
49
+ "tsup": "^6.6.3",
50
+ "typescript": "^5.7.3",
51
+ "unicodedata": "^0.1.1",
52
+ "unorm": "^1.6.0"
53
+ }
54
+ }
pnpm-lock.yaml ADDED
The diff for this file is too large to render. See raw diff
 
run.sh ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # node bot.js
2
+ # npm run start
3
+ # npx ts-node src/api
4
+ pnpm run dev
src/api/paymentWebhooks.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { telegrafBots } from '../models';
3
+ import { createLogger } from '../utils/logger';
4
+
5
+ const logger = createLogger('PaymentWebhooks');
6
+ const router = express.Router();
7
+
8
+ // Get the first bot instance from the map
9
+ const bot = Array.from(telegrafBots.values())[0];
10
+
11
+ // PayPal webhook endpoint
12
+ router.post('/paypal', async (req, res) => {
13
+ try {
14
+ const { paymentId, status } = req.body;
15
+ await bot.emit('webhook:paypal', { paymentId, status });
16
+ res.json({ success: true });
17
+ } catch (error) {
18
+ logger.error('PayPal webhook error:', error);
19
+ res.status(500).json({ success: false, error: error.message });
20
+ }
21
+ });
22
+
23
+ // Crypto webhook endpoint
24
+ router.post('/crypto', async (req, res) => {
25
+ try {
26
+ const { paymentId, status } = req.body;
27
+ await bot.emit('webhook:crypto', { paymentId, status });
28
+ res.json({ success: true });
29
+ } catch (error) {
30
+ logger.error('Crypto webhook error:', error);
31
+ res.status(500).json({ success: false, error: error.message });
32
+ }
33
+ });
34
+
35
+ // Admin payment verification endpoint
36
+ router.post('/admin', async (req, res) => {
37
+ try {
38
+ const { paymentId, status } = req.body;
39
+ await bot.emit('webhook:admin', { paymentId, status });
40
+ res.json({ success: true });
41
+ } catch (error) {
42
+ logger.error('Admin webhook error:', error);
43
+ res.status(500).json({ success: false, error: error.message });
44
+ }
45
+ });
46
+
47
+ export default router;
src/bots/botManager.ts ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Telegraf } from "telegraf";
2
+ import { telegrafBots } from "../models";
3
+ import { setupCommandHandlers } from "./handlers/commandHandlers";
4
+ import { setupCallbackHandlers } from "./handlers/index";
5
+ import { BotContext } from "./types/botTypes";
6
+ import { supabase } from "../db/supabase";
7
+ import { createLogger } from "../utils/logger";
8
+ import { messageManager } from "./utils/messageManager";
9
+ import { handleLanguageSelection, handleLanguageChange } from "./handlers/languageHandlers";
10
+
11
+ const logger = createLogger('BotManager');
12
+
13
+ export const BotCommands: { command: string; description: string }[] = [
14
+ { command: 'start', description: 'بدء البوت' },
15
+ { command: 'help', description: 'يعرض قائمة المساعدة' },
16
+ { command: 'about', description: 'معلومات عن البوت' },
17
+ { command: 'contact', description: 'تواصل معنا' },
18
+ { command: 'balance', description: 'رصيدي' },
19
+ { command: 'change_language', description: 'تغيير اللغة / Change language' },
20
+ ];
21
+
22
+ // Add this interface to match your database schema
23
+ export interface BotData {
24
+ id: string;
25
+ name: string;
26
+ user_id: string;
27
+ bot_token: string;
28
+ is_active: boolean;
29
+ currency: string;
30
+ profit_type: 'fix' | 'percentage';
31
+ profit_value_percentage: number;
32
+ profit_value_fix: number;
33
+ last_activity: string | null;
34
+ version: string;
35
+
36
+ settings: Record<string, any>;
37
+ state: Record<string, any>;
38
+ created_at: string;
39
+ updated_at: string;
40
+ }
41
+
42
+ // Update the initializeBot function to accept botData
43
+ export const initializeBot = async (botToken: string, botData?: BotData) => {
44
+ try {
45
+ const bot = new Telegraf<BotContext>(botToken);
46
+
47
+ // Set bot ID and load messages
48
+
49
+ await messageManager.loadMessages();
50
+
51
+ // Set language based on bot settings or default to Arabic
52
+ messageManager.setLanguage(botData?.settings?.language || 'en');
53
+
54
+ // Add bot data to context for use in handlers
55
+ bot.context.botData = botData || null;
56
+
57
+ // Setup middleware to track activity
58
+ bot.use(async (ctx, next) => {
59
+ if (botData?.id) {
60
+ // Update last_activity in database
61
+ await supabase
62
+ .from('bots')
63
+ .update({
64
+ last_activity: new Date().toISOString()
65
+ })
66
+ .eq('id', botData.id);
67
+ }
68
+ return next();
69
+ });
70
+
71
+ // Setup command handlers
72
+ setupCommandHandlers(bot);
73
+
74
+ // Setup callback handlers
75
+ setupCallbackHandlers(bot);
76
+
77
+ // Set bot commands
78
+ await bot.telegram.setMyCommands(BotCommands);
79
+
80
+ return {
81
+ success: true,
82
+ message: botData?.name ? `Bot "${botData.name}" initialized successfully` : "Bot initialized successfully",
83
+ bot
84
+ };
85
+ } catch (error: any) {
86
+ logger.error(`Failed to initialize bot: ${error.message}`);
87
+ return { success: false, message: `Failed to initialize bot: ${error.message}` };
88
+ }
89
+ };
90
+
91
+ export const stopBot = async (botId: string) => {
92
+ try {
93
+ // Get bot token from database
94
+ const { data: botData, error } = await supabase
95
+ .from('bots')
96
+ .select('bot_token')
97
+ .eq('id', botId)
98
+ .single();
99
+
100
+ if (error || !botData) {
101
+ return { success: false, message: "Bot not found in database" };
102
+ }
103
+
104
+ const botToken = botData.bot_token;
105
+ const bot = telegrafBots.get(botToken);
106
+
107
+ if (bot) {
108
+ await bot.stop();
109
+ telegrafBots.delete(botToken);
110
+
111
+ // Update database
112
+ await supabase
113
+ .from('bots')
114
+ .update({
115
+ is_active: false,
116
+ state: {
117
+ status: 'stopped',
118
+ stoppedAt: new Date().toISOString()
119
+ }
120
+ })
121
+ .eq('id', botId);
122
+
123
+ return { success: true, message: "Bot stopped successfully" };
124
+ }
125
+ return { success: false, message: "Bot instance not found" };
126
+ } catch (error: any) {
127
+ logger.error(`Error stopping bot: ${error.message}`);
128
+ return { success: false, message: `Failed to stop bot: ${error.message}` };
129
+ }
130
+ };
131
+
132
+ // New function to update bot settings
133
+ export const updateBotSettings = async (botId: string, settings: Record<string, any>) => {
134
+ try {
135
+ // Get bot data from database
136
+ const { data: botData, error } = await supabase
137
+ .from('bots')
138
+ .select('bot_token, settings')
139
+ .eq('id', botId)
140
+ .single();
141
+
142
+ if (error || !botData) {
143
+ return { success: false, message: "Bot not found in database" };
144
+ }
145
+
146
+ // Merge existing settings with new ones
147
+ const updatedSettings = { ...(botData.settings || {}), ...settings };
148
+
149
+ // Update database
150
+ await supabase
151
+ .from('bots')
152
+ .update({
153
+ settings: updatedSettings,
154
+ updated_at: new Date().toISOString()
155
+ })
156
+ .eq('id', botId);
157
+
158
+ // Update running bot if it exists
159
+ const bot = telegrafBots.get(botData.bot_token);
160
+ if (bot && bot.context) {
161
+ bot.context.botData = {
162
+ ...(bot.context.botData || {}),
163
+ settings: updatedSettings
164
+ };
165
+ }
166
+
167
+ return { success: true, message: "Bot settings updated successfully" };
168
+ } catch (error: any) {
169
+ logger.error(`Error updating bot settings: ${error.message}`);
170
+ return { success: false, message: `Failed to update bot settings: ${error.message}` };
171
+ }
172
+ };
173
+
src/bots/db.sql ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- users_bot_telegram table
2
+ CREATE TABLE IF NOT EXISTS public.users_bot_telegram (
3
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
4
+ telegram_id BIGINT UNIQUE NOT NULL, -- Unique Telegram user ID
5
+ username VARCHAR(255), -- Telegram username
6
+ first_name VARCHAR(255), -- User's first name
7
+ last_name VARCHAR(255), -- User's last name
8
+ email VARCHAR(255) UNIQUE NOT NULL, -- User's email address
9
+ password_hash VARCHAR(255) NOT NULL, -- Hashed password for authentication
10
+ language VARCHAR(10) NOT NULL DEFAULT 'en', -- User's preferred language
11
+ role VARCHAR(10) NOT NULL DEFAULT 'user', -- User role (user/admin/etc.)
12
+ balance DECIMAL(10, 2) NOT NULL DEFAULT 0, -- User's account balance
13
+ is_banned BOOLEAN NOT NULL DEFAULT false, -- Whether the user is banned
14
+ last_login TIMESTAMPTZ, -- Timestamp of last login
15
+ bot_id UUID REFERENCES public.bots(id) ON DELETE SET NULL, -- Reference to associated bot
16
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Timestamp when record was created
17
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- Timestamp when record was last updated
18
+ );
19
+
20
+ COMMENT ON TABLE public.users_bot_telegram IS 'Stores Telegram user accounts and their associated data.';
21
+
22
+ -- Enable Row Level Security
23
+ ALTER TABLE public.users_bot_telegram ENABLE ROW LEVEL SECURITY;
24
+
25
+ -- Policies
26
+ CREATE POLICY "Users can manage their own data." ON public.users_bot_telegram
27
+ FOR ALL USING (true);
28
+
29
+ -- Trigger for updating timestamps
30
+ CREATE TRIGGER update_users_bot_telegram_timestamp BEFORE UPDATE ON public.users_bot_telegram
31
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
32
+
33
+ -- Transactions table
34
+ CREATE TABLE IF NOT EXISTS public.transactions (
35
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
36
+ user_id INTEGER NOT NULL REFERENCES public.users_bot_telegram(id) ON DELETE CASCADE, -- Reference to user
37
+ agent_id INTEGER REFERENCES public.users_bot_telegram(id) ON DELETE SET NULL, -- Reference to agent who processed transaction
38
+ type VARCHAR(20) NOT NULL, -- Type of transaction (deposit/withdrawal/etc.)
39
+ amount DECIMAL(10, 2) NOT NULL, -- Transaction amount
40
+ reference_id VARCHAR(255), -- External reference ID
41
+ description TEXT, -- Transaction description
42
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- Timestamp when transaction was created
43
+ );
44
+
45
+ COMMENT ON TABLE public.transactions IS 'Stores financial transactions for users.';
46
+
47
+ -- Recharge cards table
48
+ CREATE TABLE IF NOT EXISTS public.recharge_cards (
49
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
50
+ code VARCHAR(50) UNIQUE NOT NULL, -- Unique card code
51
+ amount DECIMAL(10, 2) NOT NULL, -- Card value amount
52
+ is_used BOOLEAN NOT NULL DEFAULT false, -- Whether card has been used
53
+ is_reusable BOOLEAN NOT NULL DEFAULT false, -- Whether card can be reused
54
+ created_by INTEGER NOT NULL REFERENCES public.users_bot_telegram(id) ON DELETE CASCADE, -- Who created the card
55
+ used_by INTEGER REFERENCES public.users_bot_telegram(id) ON DELETE SET NULL, -- Who used the card
56
+ used_at TIMESTAMPTZ, -- When card was used
57
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When card was created
58
+ expires_at TIMESTAMPTZ -- When card expires
59
+ );
60
+
61
+ COMMENT ON TABLE public.recharge_cards IS 'Stores recharge cards for user balance top-ups.';
62
+
63
+ -- Phone numbers table
64
+ CREATE TABLE IF NOT EXISTS public.phone_numbers (
65
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
66
+ user_id INTEGER NOT NULL REFERENCES public.users_bot_telegram(id) ON DELETE CASCADE, -- Owner of the number
67
+ country_code VARCHAR(10) NOT NULL, -- Country code for number
68
+ service VARCHAR(50) NOT NULL, -- Service the number is for
69
+ number VARCHAR(50) NOT NULL, -- Phone number
70
+ price DECIMAL(10, 2) NOT NULL, -- Cost of the number
71
+ status VARCHAR(20) NOT NULL DEFAULT 'pending', -- Current status of number
72
+ fivesim_id VARCHAR(50) NOT NULL, -- Reference to 5sim.net ID
73
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When number was acquired
74
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When number was last updated
75
+ expires_at TIMESTAMPTZ -- When number expires
76
+ );
77
+
78
+ COMMENT ON TABLE public.phone_numbers IS 'Stores phone numbers acquired for verification services.';
79
+
80
+ -- Trigger for updating timestamps
81
+ CREATE TRIGGER update_phone_numbers_timestamp BEFORE UPDATE ON public.phone_numbers
82
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
83
+
84
+ -- SMS messages table
85
+ CREATE TABLE IF NOT EXISTS public.sms_messages (
86
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
87
+ phone_number_id INTEGER NOT NULL REFERENCES public.phone_numbers(id) ON DELETE CASCADE, -- Associated phone number
88
+ code VARCHAR(50) NOT NULL, -- Verification code from SMS
89
+ text TEXT NOT NULL, -- Full SMS text
90
+ received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When SMS was received
91
+ is_delivered BOOLEAN NOT NULL DEFAULT false, -- Whether SMS was delivered to user
92
+ delivered_at TIMESTAMPTZ -- When SMS was delivered to user
93
+ );
94
+
95
+ COMMENT ON TABLE public.sms_messages IS 'Stores SMS messages received for verification services.';
96
+
97
+ -- Settings table
98
+ CREATE TABLE IF NOT EXISTS public.settings (
99
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
100
+ key VARCHAR(50) UNIQUE NOT NULL, -- Setting key
101
+ value TEXT NOT NULL, -- Setting value
102
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When setting was created
103
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- When setting was last updated
104
+ );
105
+
106
+ COMMENT ON TABLE public.settings IS 'Stores system configuration settings.';
107
+
108
+ -- Trigger for updating timestamps
109
+ CREATE TRIGGER update_settings_timestamp BEFORE UPDATE ON public.settings
110
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
src/bots/handlers/balanceHandlers.ts ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { Markup } from "telegraf";
3
+ import { getLoggedInMenuKeyboard, getMainMenuKeyboard } from "../utils/keyboardUtils";
4
+ import { AuthService } from '../services/auth';
5
+ import { PayPalService } from '../services/PayPalService';
6
+ import { CryptoService } from '../services/CryptoService';
7
+ import { AdminService } from '../services/AdminService';
8
+ import { createLogger } from '../../utils/logger';
9
+ import { PaymentMethod } from '../types/paymentTypes';
10
+ import { messageManager } from "../utils/messageManager";
11
+
12
+ const logger = createLogger('BalanceHandlers');
13
+ const authService = AuthService.getInstance();
14
+ const paypalService = PayPalService.getInstance();
15
+ const cryptoService = CryptoService.getInstance();
16
+ const adminService = AdminService.getInstance();
17
+
18
+ // Add state management for custom amount
19
+ const customAmountStates = new Map<number, boolean>();
20
+
21
+ // Add payment method states
22
+ const paymentMethodStates = new Map<number, PaymentMethod>();
23
+
24
+ export const handleTopUpAction = async (ctx: BotContext) => {
25
+ const telegramId = ctx.from?.id;
26
+
27
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
28
+ return {
29
+ message: messageManager.getMessage('auth_required'),
30
+ options: getMainMenuKeyboard()
31
+ };
32
+ }
33
+
34
+ const user = await authService.getUserByTelegramId(telegramId);
35
+
36
+ if (!user) {
37
+ return {
38
+ message: messageManager.getMessage('user_not_found'),
39
+ options: getMainMenuKeyboard()
40
+ };
41
+ }
42
+
43
+ return {
44
+ message: messageManager.getMessage('balance_top_up_title') + '\n\n' +
45
+ messageManager.getMessage('balance_current_balance')
46
+ .replace('{balance}', user.balance || '0') + '\n\n' +
47
+ messageManager.getMessage('balance_choose_payment_method'),
48
+ options: {
49
+ ...Markup.inlineKeyboard([
50
+ [Markup.button.callback(messageManager.getMessage('btn_payment_paypal'), 'payment_paypal')],
51
+ [Markup.button.callback(messageManager.getMessage('btn_payment_crypto'), 'payment_crypto')],
52
+ [Markup.button.callback(messageManager.getMessage('btn_payment_admin'), 'payment_admin')],
53
+ [Markup.button.callback(messageManager.getMessage('btn_back_to_main'), 'main_menu')]
54
+ ]),
55
+ parse_mode: 'HTML'
56
+ }
57
+ };
58
+ };
59
+
60
+ export const handleCustomAmountRequest = async (ctx: BotContext) => {
61
+ const telegramId = ctx.from?.id;
62
+
63
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
64
+ return {
65
+ message: messageManager.getMessage('auth_required'),
66
+ options: getMainMenuKeyboard()
67
+ };
68
+ }
69
+
70
+ customAmountStates.set(telegramId, true);
71
+
72
+ return {
73
+ message: messageManager.getMessage('balance_custom_amount_title') + '\n\n' +
74
+ messageManager.getMessage('balance_enter_custom_amount'),
75
+ options: {
76
+ ...Markup.inlineKeyboard([
77
+ [Markup.button.callback(messageManager.getMessage('btn_cancel'), 'top_up_balance')]
78
+ ]),
79
+ parse_mode: 'HTML'
80
+ }
81
+ };
82
+ };
83
+
84
+ export const handlePaymentMethodSelection = async (ctx: BotContext, method: PaymentMethod) => {
85
+ const telegramId = ctx.from?.id;
86
+
87
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
88
+ return {
89
+ message: messageManager.getMessage('auth_required'),
90
+ options: getMainMenuKeyboard()
91
+ };
92
+ }
93
+
94
+ paymentMethodStates.set(telegramId, method);
95
+
96
+ return {
97
+ message: messageManager.getMessage('balance_top_up_title') + '\n\n' +
98
+ messageManager.getMessage('balance_choose_amount'),
99
+ options: {
100
+ ...Markup.inlineKeyboard([
101
+ [
102
+ Markup.button.callback(messageManager.getMessage('btn_amount_5'), 'top_up_5'),
103
+ Markup.button.callback(messageManager.getMessage('btn_amount_10'), 'top_up_10'),
104
+ Markup.button.callback(messageManager.getMessage('btn_amount_20'), 'top_up_20')
105
+ ],
106
+ [
107
+ Markup.button.callback(messageManager.getMessage('btn_amount_50'), 'top_up_50'),
108
+ Markup.button.callback(messageManager.getMessage('btn_amount_100'), 'top_up_100')
109
+ ],
110
+ [Markup.button.callback(messageManager.getMessage('btn_custom_amount'), 'top_up_custom')],
111
+ [Markup.button.callback(messageManager.getMessage('btn_back_to_balance'), 'top_up_balance')]
112
+ ]),
113
+ parse_mode: 'HTML'
114
+ }
115
+ };
116
+ };
117
+
118
+ export const handleTopUpAmount = async (ctx: BotContext, amount: number) => {
119
+ const telegramId = ctx.from?.id;
120
+
121
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
122
+ return {
123
+ message: messageManager.getMessage('auth_required'),
124
+ options: getMainMenuKeyboard()
125
+ };
126
+ }
127
+
128
+ const user = await authService.getUserByTelegramId(telegramId);
129
+ const paymentMethod = paymentMethodStates.get(telegramId);
130
+
131
+ if (!user) {
132
+ return {
133
+ message: messageManager.getMessage('user_not_found'),
134
+ options: getMainMenuKeyboard()
135
+ };
136
+ }
137
+
138
+ try {
139
+ let message: string;
140
+
141
+ switch (paymentMethod) {
142
+ case 'PAYPAL':
143
+ const paymentLink = await paypalService.createPaymentLink(user.id, amount);
144
+ message = messageManager.getMessage('payment_paypal_title') + '\n\n' +
145
+ `المبلغ المطلوب: $${amount}\n\n` +
146
+ messageManager.getMessage('payment_paypal_instructions')
147
+ .replace('{payment_link}', paymentLink);
148
+ break;
149
+
150
+ case 'CRYPTO':
151
+ const cryptoAddress = await cryptoService.getPaymentAddress(user.id, amount);
152
+ message = messageManager.getMessage('payment_crypto_title') + '\n\n' +
153
+ `المبلغ المطلوب: $${amount}\n\n` +
154
+ messageManager.getMessage('payment_crypto_instructions')
155
+ .replace('{wallet_address}', cryptoAddress);
156
+ break;
157
+
158
+ case 'ADMIN':
159
+ const adminContact = await adminService.getAdminContact();
160
+ message = messageManager.getMessage('payment_admin_title') + '\n\n' +
161
+ `المبلغ المطلوب: $${amount}\n\n` +
162
+ messageManager.getMessage('payment_admin_instructions')
163
+ .replace('{admin_contact}', adminContact);
164
+ break;
165
+
166
+ default:
167
+ throw new Error('Invalid payment method');
168
+ }
169
+
170
+ return {
171
+ message: message,
172
+ options: {
173
+ ...Markup.inlineKeyboard([
174
+ [Markup.button.callback(messageManager.getMessage('btn_back_to_main'), 'main_menu')]
175
+ ]),
176
+ parse_mode: 'HTML'
177
+ }
178
+ };
179
+ } catch (error) {
180
+ logger.error('Error creating payment:', error);
181
+ return {
182
+ message: messageManager.getMessage('payment_error'),
183
+ options: getLoggedInMenuKeyboard()
184
+ };
185
+ }
186
+ };
187
+
188
+
189
+
190
+ // Add handler for custom amount input
191
+ export const handleCustomAmountInput = async (ctx: BotContext) => {
192
+ const telegramId = ctx.from?.id;
193
+
194
+ if (!telegramId || !customAmountStates.get(telegramId)) {
195
+ return;
196
+ }
197
+
198
+ const amountText = ctx.message?.text;
199
+ if (!amountText) return;
200
+
201
+ // Remove state
202
+ customAmountStates.delete(telegramId)
203
+
204
+ // Parse amount
205
+ const amount = parseFloat(amountText);
206
+ if (isNaN(amount) || amount <= 0) {
207
+ return {
208
+ message: "⚠️ المبلغ غير صالح. الرجاء إدخال رقم صحيح أو عشري موجب.",
209
+ options: {
210
+ ...Markup.inlineKeyboard([
211
+ [Markup.button.callback('🔙 العودة', 'top_up_balance')]
212
+ ]),
213
+ parse_mode: 'HTML'
214
+ }
215
+ };
216
+ }
217
+
218
+ // Handle the custom amount
219
+ return await handleTopUpAmount(ctx, amount);
220
+ };
221
+ export const setupBalanceHandlers = (bot: any) => {
222
+ // Handle top-up button click
223
+ bot.action('top_up_balance', async (ctx: BotContext) => {
224
+ const result = await handleTopUpAction(ctx);
225
+ await ctx.editMessageText(result.message, result.options);
226
+ });
227
+
228
+ // Add custom amount handler
229
+ bot.action('top_up_custom', async (ctx: BotContext) => {
230
+ const result = await handleCustomAmountRequest(ctx);
231
+ await ctx.editMessageText(result.message, result.options);
232
+ });
233
+
234
+ // Handle text messages for custom amount
235
+ bot.on('text', async (ctx: BotContext) => {
236
+ const result = await handleCustomAmountInput(ctx);
237
+ if (result) {
238
+ await ctx.reply(result.message, result.options);
239
+ }
240
+ });
241
+
242
+ // Handle specific amount selections
243
+ bot.action('top_up_5', async (ctx: BotContext) => {
244
+ const result = await handleTopUpAmount(ctx, 5);
245
+ await ctx.editMessageText(result.message, result.options);
246
+ });
247
+
248
+ bot.action('top_up_10', async (ctx: BotContext) => {
249
+ const result = await handleTopUpAmount(ctx, 10);
250
+ await ctx.editMessageText(result.message, result.options);
251
+ });
252
+
253
+ bot.action('top_up_20', async (ctx: BotContext) => {
254
+ const result = await handleTopUpAmount(ctx, 20);
255
+ await ctx.editMessageText(result.message, result.options);
256
+ });
257
+
258
+ bot.action('top_up_50', async (ctx: BotContext) => {
259
+ const result = await handleTopUpAmount(ctx, 50);
260
+ await ctx.editMessageText(result.message, result.options);
261
+ });
262
+
263
+ bot.action('top_up_100', async (ctx: BotContext) => {
264
+ const result = await handleTopUpAmount(ctx, 100);
265
+ await ctx.editMessageText(result.message, result.options);
266
+ });
267
+
268
+ // Add payment method handlers
269
+ bot.action('payment_paypal', async (ctx: BotContext) => {
270
+ const result = await handlePaymentMethodSelection(ctx, 'PAYPAL');
271
+ await ctx.editMessageText(result.message, result.options);
272
+ });
273
+
274
+ bot.action('payment_crypto', async (ctx: BotContext) => {
275
+ const result = await handlePaymentMethodSelection(ctx, 'CRYPTO');
276
+ await ctx.editMessageText(result.message, result.options);
277
+ });
278
+
279
+ bot.action('payment_admin', async (ctx: BotContext) => {
280
+ const result = await handlePaymentMethodSelection(ctx, 'ADMIN');
281
+ await ctx.editMessageText(result.message, result.options);
282
+ });
283
+ };
src/bots/handlers/commandHandlers.ts ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getWelcomeMessage , getHelpMessage, getAboutMessage, getContactMessage} from "../utils/messageUtils";
2
+ import { getMainMenuKeyboard, getLoggedInMenuKeyboard, getHistoryKeyboard } from "../utils/keyboardUtils";
3
+ import { BotContext } from "../types/botTypes";
4
+ import { AuthService } from '../services/auth';
5
+ import { PurchaseTrackingService, PurchaseState } from '../services/PurchaseTrackingService';
6
+ import { createLogger } from '../../utils/logger';
7
+ import { messageReplyHandler } from "../utils/handlerUtils";
8
+ import { handleLanguageSelection } from "./languageHandlers";
9
+ import { messageManager } from "../utils/messageManager";
10
+ // import { logger } from "../../utils/logger";
11
+
12
+ const logger = createLogger('CommandHandlers');
13
+ const authService = AuthService.getInstance();
14
+ const purchaseTrackingService = PurchaseTrackingService.getInstance();
15
+
16
+ export const setupCommandHandlers = (bot: any) => {
17
+ // /start command handler
18
+ bot.start(messageReplyHandler(handleStartCommand));
19
+
20
+ // Other commands using the new messageReplyHandler
21
+ bot.command('help', messageReplyHandler(handleHelpCommand));
22
+ bot.command('about', messageReplyHandler(handleAboutCommand));
23
+ bot.command('contact', messageReplyHandler(handleContactCommand));
24
+ bot.command('balance', messageReplyHandler(handleBalanceCommand));
25
+ bot.command('history', messageReplyHandler(handleHistoryCommand));
26
+
27
+ // Add language command handler
28
+ bot.command('change_language', messageReplyHandler(handleLanguageCommand));
29
+ };
30
+
31
+ const handleStartCommand = (ctx: BotContext) => {
32
+ const name = ctx.from?.first_name || "عزيزي المستخدم";
33
+ const telegramId = ctx.from?.id;
34
+
35
+ if (telegramId && authService.isUserLoggedIn(telegramId)) {
36
+ return {
37
+ message: messageManager.getMessage('start_welcome_back').replace('{name}', name),
38
+ options: getLoggedInMenuKeyboard()
39
+ };
40
+ } else {
41
+ return {
42
+ message: messageManager.getMessage('start_welcome_new').replace('{name}', name),
43
+ options: getMainMenuKeyboard()
44
+ };
45
+ }
46
+ };
47
+
48
+ const handleAboutCommand = (ctx: BotContext) => {
49
+ const botData = ctx.botData;
50
+
51
+ return {
52
+ message: [
53
+ messageManager.getMessage('about_title'),
54
+ messageManager.getMessage('about_bot_name').replace('{bot_name}', botData?.name || 'بوت الأرقام الافتراضية'),
55
+ messageManager.getMessage('about_version').replace('{version}', botData?.version || '1.0.0'),
56
+ messageManager.getMessage('about_currency').replace('{currency}', botData?.currency || 'USD'),
57
+ '',
58
+ messageManager.getMessage('about_features'),
59
+ '',
60
+ messageManager.getMessage('about_copyright')
61
+ ].join('\n'),
62
+ options: { parse_mode: 'HTML' }
63
+ };
64
+ };
65
+
66
+
67
+ const handleHelpCommand = (ctx: BotContext) => {
68
+ return {
69
+ message: getHelpMessage(),
70
+ options: { parse_mode: 'HTML' }
71
+ };
72
+ };
73
+
74
+ const handleContactCommand = (ctx: BotContext) => {
75
+ return {
76
+ message: getContactMessage(),
77
+ options: { parse_mode: 'HTML' }
78
+ };
79
+ };
80
+
81
+ const handleBalanceCommand = async (ctx: BotContext) => {
82
+ const telegramId = ctx.from?.id;
83
+
84
+ if (!telegramId) {
85
+ return {
86
+ message: messageManager.getMessage('error_user_not_found'),
87
+ options: {}
88
+ };
89
+ }
90
+
91
+ const user = await authService.getUserByTelegramId(telegramId);
92
+
93
+ if (!user) {
94
+ return {
95
+ message: messageManager.getMessage('balance_auth_required'),
96
+ options: getMainMenuKeyboard()
97
+ };
98
+ }
99
+
100
+ return {
101
+ message: [
102
+ messageManager.getMessage('balance_title'),
103
+ '',
104
+ messageManager.getMessage('balance_current').replace('{balance}', user.balance || '0'),
105
+ messageManager.getMessage('balance_last_updated').replace('{update_time}', new Date().toLocaleString('ar-SA'))
106
+ ].join('\n'),
107
+ options: {
108
+ ...getLoggedInMenuKeyboard(),
109
+ parse_mode: 'HTML'
110
+ }
111
+ };
112
+ };
113
+
114
+ const handleHistoryCommand = async (ctx: BotContext) => {
115
+ const telegramId = ctx.from?.id;
116
+
117
+ if (!telegramId) {
118
+ return {
119
+ message: messageManager.getMessage('error_user_not_found'),
120
+ options: {}
121
+ };
122
+ }
123
+
124
+ if (!authService.isUserLoggedIn(telegramId)) {
125
+ return {
126
+ message: messageManager.getMessage('history_auth_required'),
127
+ options: getMainMenuKeyboard()
128
+ };
129
+ }
130
+
131
+ return {
132
+ message: [
133
+ messageManager.getMessage('history_title'),
134
+ '',
135
+ messageManager.getMessage('history_description')
136
+ ].join('\n'),
137
+ options: {
138
+ ...getHistoryKeyboard(),
139
+ parse_mode: 'HTML'
140
+ }
141
+ };
142
+ };
143
+
144
+
145
+ const handleLanguageCommand = (ctx: BotContext) => {
146
+ return handleLanguageSelection(ctx);
147
+ };
148
+
149
+
150
+
src/bots/handlers/giftHandlers.ts ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import { Markup } from 'telegraf';
3
+ import { authService } from '../services/auth';
4
+ import { supabase } from '../../db/supabase';
5
+ import { createLogger } from '../../utils/logger';
6
+ import { messageManager } from '../utils/messageManager';
7
+ import { getMainMenuKeyboard, getProfileKeyboard } from '../utils/keyboardUtils';
8
+ const logger = createLogger('GiftHandlers');
9
+ // Store gift states
10
+ export const giftStates = new Map<number, { step: number; recipientEmail?: string; amount?: number }>();
11
+
12
+ export const handleGiftBalanceAction = async (ctx: BotContext) => {
13
+ const telegramId = ctx.from?.id;
14
+
15
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
16
+ return {
17
+ message: messageManager.getMessage('auth_required'),
18
+ options: getMainMenuKeyboard()
19
+ };
20
+ }
21
+
22
+ // Set initial gift state
23
+ giftStates.set(telegramId, { step: 1 });
24
+
25
+ return {
26
+ message: "🎁 <b>إرسال هدية</b>\n\n" +
27
+ "الرجاء إدخال البريد الإلكتروني للمستلم:",
28
+ options: {
29
+ ...getProfileKeyboard(),
30
+ parse_mode: 'HTML'
31
+ }
32
+ };
33
+ };
34
+
35
+ export const handleGiftEmailInput = async (ctx: BotContext) => {
36
+ const telegramId = ctx.from?.id;
37
+
38
+ if (!telegramId || !giftStates.get(telegramId)) {
39
+ return;
40
+ }
41
+
42
+ const message = ctx.message;
43
+ const email = (message && 'text' in message) ? message.text : undefined;
44
+ if (!email) return;
45
+
46
+ // Validate email
47
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
48
+ if (!emailRegex.test(email)) {
49
+ return {
50
+ message: "❌ البريد الإلكتروني غير صالح. الرجاء إدخال بريد إلكتروني صحيح.",
51
+ options: {
52
+ ...getProfileKeyboard(),
53
+ parse_mode: 'HTML'
54
+ }
55
+ };
56
+ }
57
+
58
+ // Update state with recipient email
59
+ giftStates.set(telegramId, { step: 2, recipientEmail: email });
60
+
61
+ return {
62
+ message: "💰 <b>إدخال المبلغ</b>\n\n" +
63
+ "الرجاء إدخال المبلغ الذي تريد إرساله:",
64
+ options: {
65
+ ...getProfileKeyboard(),
66
+ parse_mode: 'HTML'
67
+ }
68
+ };
69
+ };
70
+
71
+ export const handleGiftAmountInput = async (ctx: BotContext) => {
72
+ const telegramId = ctx.from?.id;
73
+
74
+ if (!telegramId || !giftStates.get(telegramId)) {
75
+ return;
76
+ }
77
+
78
+ const message = ctx.message;
79
+ const amountText = (message && 'text' in message) ? message.text : undefined;
80
+ if (!amountText) return;
81
+
82
+ const amount = parseFloat(amountText);
83
+ if (isNaN(amount) || amount <= 0) {
84
+ return {
85
+ message: "❌ المبلغ غير صالح. الرجاء إدخال رقم صحيح أكبر من صفر.",
86
+ options: {
87
+ ...getProfileKeyboard(),
88
+ parse_mode: 'HTML'
89
+ }
90
+ };
91
+ }
92
+
93
+ const state = giftStates.get(telegramId);
94
+ if (!state?.recipientEmail) return;
95
+
96
+ try {
97
+ // Get sender's current balance
98
+ const sender = await authService.getUserByTelegramId(telegramId);
99
+ if (!sender || (sender.balance || 0) < amount) {
100
+ return {
101
+ message: "❌ رصيدك غير كافي لإتمام هذه العملية.",
102
+ options: {
103
+ ...getProfileKeyboard(),
104
+ parse_mode: 'HTML'
105
+ }
106
+ };
107
+ }
108
+
109
+ // Get recipient's information
110
+ const { data: recipient, error: recipientError } = await supabase
111
+ .from('users_bot_telegram')
112
+ .select('*')
113
+ .eq('email', state.recipientEmail)
114
+ .single();
115
+
116
+ if (recipientError || !recipient) {
117
+ return {
118
+ message: "❌ لم يتم العثور على المستخدم بهذا البريد الإلكتروني.",
119
+ options: {
120
+ ...getProfileKeyboard(),
121
+ parse_mode: 'HTML'
122
+ }
123
+ };
124
+ }
125
+
126
+ // Update balances
127
+ const { error: updateError } = await supabase
128
+ .from('users_bot_telegram')
129
+ .update({
130
+ balance: (sender.balance || 0) - amount
131
+ })
132
+ .eq('telegram_id', telegramId);
133
+
134
+ if (updateError) throw updateError;
135
+
136
+ const { error: recipientUpdateError } = await supabase
137
+ .from('users_bot_telegram')
138
+ .update({
139
+ balance: (recipient.balance || 0) + amount
140
+ })
141
+ .eq('id', recipient.id);
142
+
143
+ if (recipientUpdateError) throw recipientUpdateError;
144
+
145
+ // Clear gift state
146
+ giftStates.delete(telegramId);
147
+
148
+ // Notify recipient
149
+ if (recipient.telegram_id) {
150
+ await ctx.telegram.sendMessage(
151
+ recipient.telegram_id,
152
+ `🎁 <b>لقد تلقيت هدية!</b>\n\n` +
153
+ `تم إضافة ${amount}$ إلى رصيدك من ${sender.firstName}.`,
154
+ { parse_mode: 'HTML' }
155
+ );
156
+ }
157
+
158
+ return {
159
+ message: `✅ تم إرسال الهدية بنجاح!\n\nتم خصم ${amount}$ من رصيدك وإرسالها إلى ${state.recipientEmail}`,
160
+ options: {
161
+ ...getProfileKeyboard(),
162
+ parse_mode: 'HTML'
163
+ }
164
+ };
165
+ } catch (error) {
166
+ logger.error(`Error processing gift: ${error}`);
167
+ return {
168
+ message: "❌ حدث خطأ أثناء معالجة الهدية. الرجاء المحاولة مرة أخرى.",
169
+ options: {
170
+ ...getProfileKeyboard(),
171
+ parse_mode: 'HTML'
172
+ }
173
+ };
174
+ }
175
+ };
src/bots/handlers/historyHandlers.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { AuthService } from '../services/auth';
3
+ import { PurchaseTrackingService, PurchaseState } from '../services/PurchaseTrackingService';
4
+ import { createLogger } from '../../utils/logger';
5
+ import { callbackReplyHandler } from "../utils/handlerUtils";
6
+ import { getAuthRequiredMessage } from "../utils/messageUtils";
7
+ import {
8
+ getLoggedInMenuKeyboard,
9
+ getMainMenuKeyboard,
10
+ getHistoryKeyboard
11
+ } from "../utils/keyboardUtils";
12
+ import { messageManager } from "../utils/messageManager";
13
+
14
+ const logger = createLogger('HistoryHandlers');
15
+ const authService = AuthService.getInstance();
16
+ const purchaseTrackingService = PurchaseTrackingService.getInstance();
17
+
18
+ export const setupHistoryHandlers = (bot: any) => {
19
+ // History handlers
20
+ bot.action('history', callbackReplyHandler(handleHistoryAction));
21
+ bot.action('numbers_history', callbackReplyHandler(handleNumbersHistoryAction));
22
+ bot.action('purchases_history', callbackReplyHandler(handlePurchasesHistoryAction));
23
+ };
24
+ export const handleHistoryAction = async (ctx: BotContext) => {
25
+ const telegramId = ctx.from?.id;
26
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
27
+ return {
28
+ message: messageManager.getMessage('auth_required'),
29
+ options: getMainMenuKeyboard()
30
+ };
31
+ }
32
+
33
+ return {
34
+ message: messageManager.getMessage('history_title') + '\n\n' +
35
+ messageManager.getMessage('history_description'),
36
+ options: getHistoryKeyboard()
37
+ };
38
+ };
39
+
40
+ export const handleNumbersHistoryAction = async (ctx: BotContext) => {
41
+ const telegramId = ctx.from?.id;
42
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
43
+ return {
44
+ message: messageManager.getMessage('auth_required'),
45
+ options: getMainMenuKeyboard()
46
+ };
47
+ }
48
+
49
+ try {
50
+ const purchases = await purchaseTrackingService.getUserPurchasesByState(telegramId, PurchaseState.SUCCESS);
51
+
52
+ if (!purchases || purchases.length === 0) {
53
+ return {
54
+ message: messageManager.getMessage('history_numbers_title') + '\n\n' +
55
+ messageManager.getMessage('history_numbers_empty'),
56
+ options: getLoggedInMenuKeyboard()
57
+ };
58
+ }
59
+
60
+ let message = messageManager.getMessage('history_numbers_title') + '\n\n';
61
+
62
+ purchases.slice(0, 10).forEach((purchase, index) => {
63
+ message += messageManager.getMessage('history_item_format')
64
+ .replace('{index}', (index + 1).toString())
65
+ .replace('{service}', purchase.service)
66
+ .replace('{state_emoji}', messageManager.getMessage('purchase_state_success'))
67
+ .replace('{phone_number}', purchase.phoneNumber || 'N/A')
68
+ .replace('{country}', purchase.countryId)
69
+ .replace('{operator}', purchase.operator)
70
+ .replace('{cost}', purchase.cost.toString())
71
+ .replace('{purchase_date}', new Date(purchase.createdAt || '').toLocaleString('ar-SA')) + '\n\n';
72
+ });
73
+
74
+ if (purchases.length > 10) {
75
+ message += messageManager.getMessage('history_more_items')
76
+ .replace('{count}', (purchases.length - 10).toString())
77
+ .replace('{type}', 'أرقام') + '\n\n';
78
+ }
79
+
80
+ return {
81
+ message: message,
82
+ options: {
83
+ parse_mode: 'HTML',
84
+ ...getHistoryKeyboard()
85
+ }
86
+ };
87
+
88
+ } catch (error: any) {
89
+ logger.error(`Error fetching numbers history for user ${telegramId}: ${error.message}`);
90
+ return {
91
+ message: messageManager.getMessage('history_numbers_error'),
92
+ options: getLoggedInMenuKeyboard()
93
+ };
94
+ }
95
+ };
96
+
97
+ export const handlePurchasesHistoryAction = async (ctx: BotContext) => {
98
+ const telegramId = ctx.from?.id;
99
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
100
+ return {
101
+ message: messageManager.getMessage('auth_required'),
102
+ options: getMainMenuKeyboard()
103
+ };
104
+ }
105
+
106
+ try {
107
+ const purchases = await purchaseTrackingService.getUserPurchases(telegramId);
108
+
109
+ if (!purchases || purchases.length === 0) {
110
+ return {
111
+ message: messageManager.getMessage('history_purchases_title') + '\n\n' +
112
+ messageManager.getMessage('history_purchases_empty'),
113
+ options: getLoggedInMenuKeyboard()
114
+ };
115
+ }
116
+
117
+ let message = messageManager.getMessage('history_purchases_title') + '\n\n';
118
+
119
+ purchases.slice(0, 10).forEach((purchase, index) => {
120
+ const stateEmoji = messageManager.getMessage(`purchase_state_${purchase.state.toLowerCase()}`) ||
121
+ messageManager.getMessage('purchase_state_unknown');
122
+
123
+ message += messageManager.getMessage('history_item_format')
124
+ .replace('{index}', (index + 1).toString())
125
+ .replace('{service}', purchase.service)
126
+ .replace('{state_emoji}', stateEmoji)
127
+ .replace('{phone_number}', purchase.phoneNumber || 'N/A')
128
+ .replace('{country}', purchase.countryId)
129
+ .replace('{operator}', purchase.operator)
130
+ .replace('{cost}', purchase.cost.toString())
131
+ .replace('{purchase_date}', new Date(purchase.createdAt || '').toLocaleString('ar-SA')) + '\n\n';
132
+ });
133
+
134
+ if (purchases.length > 10) {
135
+ message += messageManager.getMessage('history_more_items')
136
+ .replace('{count}', (purchases.length - 10).toString())
137
+ .replace('{type}', 'عمليات') + '\n\n';
138
+ }
139
+
140
+ return {
141
+ message: message,
142
+ options: {
143
+ parse_mode: 'HTML',
144
+ ...getHistoryKeyboard()
145
+ }
146
+ };
147
+
148
+ } catch (error: any) {
149
+ logger.error(`Error fetching purchases history for user ${telegramId}: ${error.message}`);
150
+ return {
151
+ message: messageManager.getMessage('history_purchases_error'),
152
+ options: getLoggedInMenuKeyboard()
153
+ };
154
+ }
155
+ };
src/bots/handlers/index.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { setupMainMenuHandlers } from './mainMenuHandlers';
2
+ import { setupServiceHandlers } from './serviceHandlers';
3
+ import { setupPurchaseHandlers } from './purchaseHandlers';
4
+ import { setupHistoryHandlers } from './historyHandlers';
5
+ import { setupBalanceHandlers } from './balanceHandlers';
6
+ import { setupLanguageHandlers } from './languageHandlers';
7
+ import { setupProfileHandlers } from './profileHandlers';
8
+ import { handleGiftAmountInput, handleGiftEmailInput, giftStates, handleGiftBalanceAction } from './giftHandlers';
9
+ import { BotContext } from '../types/botTypes';
10
+
11
+ export const setupCallbackHandlers = (bot: any) => {
12
+ setupMainMenuHandlers(bot);
13
+ setupServiceHandlers(bot);
14
+ setupPurchaseHandlers(bot);
15
+ setupHistoryHandlers(bot);
16
+ setupBalanceHandlers(bot);
17
+ setupLanguageHandlers(bot);
18
+ setupProfileHandlers(bot);
19
+
20
+ bot.action('gift_balance', async (ctx: BotContext) => {
21
+ const result = await handleGiftBalanceAction(ctx);
22
+ await ctx.editMessageText(result.message, result.options);
23
+ });
24
+
25
+ // Handle text messages for gift process
26
+ bot.on('text', async (ctx: BotContext) => {
27
+ const telegramId = ctx.from?.id;
28
+ if (!telegramId) return;
29
+
30
+ const giftState = giftStates.get(telegramId);
31
+ if (!giftState) return;
32
+
33
+ let result;
34
+ if (giftState.step === 1) {
35
+ result = await handleGiftEmailInput(ctx);
36
+ } else if (giftState.step === 2) {
37
+ result = await handleGiftAmountInput(ctx);
38
+ }
39
+
40
+ if (result) {
41
+ await ctx.reply(result.message, result.options);
42
+ }
43
+ });
44
+ };
src/bots/handlers/languageHandlers.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import { Markup } from 'telegraf';
3
+ import { getLanguageSelectionKeyboard, getMainMenuKeyboard, getLoggedInMenuKeyboard } from '../utils/keyboardUtils';
4
+ import { authService } from '../services/auth';
5
+ import { messageManager } from '../utils/messageManager';
6
+ import { supabase } from '../../db/supabase';
7
+
8
+ export const setupLanguageHandlers = (bot: any) => {
9
+
10
+
11
+ // Language selection handlers
12
+ bot.action('change_language', async (ctx) => {
13
+ const result = await handleLanguageSelection(ctx);
14
+ await ctx.editMessageText(result.message, result.options);
15
+ });
16
+
17
+ bot.action('set_language_en', async (ctx) => {
18
+ const result = await handleLanguageChange(ctx, 'en');
19
+ await ctx.editMessageText(result.message, result.options);
20
+ });
21
+
22
+ bot.action('set_language_ar', async (ctx) => {
23
+ const result = await handleLanguageChange(ctx, 'ar');
24
+ await ctx.editMessageText(result.message, result.options);
25
+ });
26
+ };
27
+
28
+ export const handleLanguageSelection = async (ctx: BotContext) => {
29
+ const telegramId = ctx.from?.id;
30
+ const isLoggedIn = authService.isUserLoggedIn(telegramId) ;
31
+
32
+ return {
33
+ message: "🌐 Choose your preferred language:\nاختر لغتك المفضلة:",
34
+ options: getLanguageSelectionKeyboard(isLoggedIn)
35
+ };
36
+ };
37
+
38
+ export const handleLanguageChange = async (ctx: BotContext, language: 'en' | 'ar') => {
39
+ const telegramId = ctx.from?.id;
40
+ const isLoggedIn = telegramId ? authService.isUserLoggedIn(telegramId) : false;
41
+
42
+ try {
43
+ // Update message manager language for current session
44
+ messageManager.setLanguage(language);
45
+
46
+ // If user is logged in, update their language preference in database
47
+ if (isLoggedIn && telegramId) {
48
+ const { error } = await supabase
49
+ .from('users_bot_telegram')
50
+ .update({ language })
51
+ .eq('telegram_id', telegramId);
52
+
53
+ if (error) throw error;
54
+ }
55
+
56
+ const successMessage = language === 'en'
57
+ ? "✅ Language changed to English"
58
+ : "✅ تم تغيير اللغة إلى العربية";
59
+
60
+ return {
61
+ message: successMessage,
62
+ options: isLoggedIn ? getLoggedInMenuKeyboard() : getMainMenuKeyboard()
63
+ };
64
+ } catch (error: any) {
65
+ const errorMessage = language === 'en'
66
+ ? "❌ Failed to change language"
67
+ : "❌ فشل تغيير اللغة";
68
+
69
+ return {
70
+ message: errorMessage,
71
+ options: isLoggedIn ? getLoggedInMenuKeyboard() : getMainMenuKeyboard()
72
+ };
73
+ }
74
+ };
src/bots/handlers/mainMenuHandlers.ts ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { AuthService } from '../services/auth';
3
+ import { createLogger } from '../../utils/logger';
4
+ import { callbackReplyHandler } from "../utils/handlerUtils";
5
+ import {
6
+ getWelcomeMessage,
7
+ getTermsMessage,
8
+ getNewMembersMessage,
9
+ getStatsMessage,
10
+ getAuthRequiredMessage
11
+ } from "../utils/messageUtils";
12
+ import {
13
+ getBackToMainMenuButton,
14
+ getLoggedInMenuKeyboard,
15
+ getMainMenuKeyboard
16
+ } from "../utils/keyboardUtils";
17
+ import { fetchDataFromTable } from '../../db/supabaseHelper';
18
+ import { messageManager } from "../utils/messageManager";
19
+
20
+ const logger = createLogger('MainMenuHandlers');
21
+ const authService = AuthService.getInstance();
22
+
23
+ export const setupMainMenuHandlers = (bot: any) => {
24
+ bot.action('login', callbackReplyHandler(handleLoginAction));
25
+ bot.action('terms', callbackReplyHandler(handleTermsAction));
26
+ bot.action('new_members', callbackReplyHandler(handleNewMembersAction));
27
+ bot.action('stats', callbackReplyHandler(handleStatsAction));
28
+ bot.action('main_menu', callbackReplyHandler(handleMainMenuAction));
29
+ };
30
+ export const handleLoginAction = async (ctx: BotContext) => {
31
+ try {
32
+ const telegramId = ctx.from?.id;
33
+ const firstName = ctx.from?.first_name || '';
34
+
35
+ if (!telegramId) {
36
+ return {
37
+ message: messageManager.getMessage('login_error_user_info'),
38
+ options: getBackToMainMenuButton()
39
+ };
40
+ }
41
+
42
+ if (authService.isUserLoggedIn(telegramId)) {
43
+ return {
44
+ message: messageManager.getMessage('login_already_logged_in'),
45
+ options: getLoggedInMenuKeyboard()
46
+ };
47
+ }
48
+
49
+ const botToken = (ctx as any).telegram?.token;
50
+ if (!botToken) {
51
+ logger.error('Bot token not found in context');
52
+ return {
53
+ message: messageManager.getMessage('login_error_system'),
54
+ options: getBackToMainMenuButton()
55
+ };
56
+ }
57
+
58
+ let user = await authService.loginUser(telegramId);
59
+
60
+ if (user) {
61
+ logger.info(`User ${telegramId} logged in successfully`);
62
+ return {
63
+ message: messageManager.getMessage('login_success')
64
+ .replace('{firstName}', user.firstName)
65
+ .replace('{email}', user.email)
66
+ .replace('{balance}', user.balance?.toString() || '0'),
67
+ options: getLoggedInMenuKeyboard()
68
+ };
69
+ } else {
70
+ try {
71
+ const { data: bots } = await fetchDataFromTable('bots', 1000, 0, { bot_token: botToken });
72
+ const botId = bots && bots.length > 0 ? bots[0].id : null;
73
+
74
+ if (!botId) {
75
+ logger.error(`Bot not found for token ${botToken}`);
76
+ return {
77
+ message: messageManager.getMessage('login_error_system'),
78
+ options: getBackToMainMenuButton()
79
+ };
80
+ }
81
+
82
+ const { user: newUser, password } = await authService.createUser(telegramId, firstName, botId);
83
+ authService.setUserLoggedIn(telegramId, true);
84
+
85
+ logger.info(`New user ${telegramId} created successfully with bot ID ${botId}`);
86
+ return {
87
+ message: messageManager.getMessage('account_created_success')
88
+ .replace('{firstName}', firstName)
89
+ .replace('{email}', newUser.email)
90
+ .replace('{password}', password),
91
+ options: getLoggedInMenuKeyboard()
92
+ };
93
+ } catch (error: any) {
94
+ logger.error(`Error creating new user: ${error.message}`);
95
+ return {
96
+ message: messageManager.getMessage('login_error_create_account'),
97
+ options: getBackToMainMenuButton()
98
+ };
99
+ }
100
+ }
101
+ } catch (error: any) {
102
+ logger.error(`Error in login action: ${error.message}`);
103
+ return {
104
+ message: messageManager.getMessage('login_error_general'),
105
+ options: getBackToMainMenuButton()
106
+ };
107
+ }
108
+ };
109
+ export const handleTermsAction = async (ctx: BotContext) => {
110
+ return {
111
+ message: getTermsMessage(),
112
+ options: { parse_mode: 'HTML' }
113
+ };
114
+ };
115
+
116
+ export const handleNewMembersAction = async (ctx: BotContext) => {
117
+ return {
118
+ message: getNewMembersMessage(),
119
+ options: { parse_mode: 'HTML' }
120
+ };
121
+ };
122
+
123
+ export const handleStatsAction = async (ctx: BotContext) => {
124
+ return {
125
+ message: getStatsMessage(),
126
+ options: { parse_mode: 'HTML' }
127
+ };
128
+ };
129
+
130
+ export const handleMainMenuAction = async (ctx: BotContext) => {
131
+ const name = ctx.from?.first_name || messageManager.getMessage('default_user_name');
132
+ const telegramId = ctx.from?.id;
133
+
134
+ if (telegramId && authService.isUserLoggedIn(telegramId)) {
135
+ return {
136
+ message: messageManager.getMessage('main_menu_welcome_back').replace('{name}', name),
137
+ options: getLoggedInMenuKeyboard()
138
+ };
139
+ } else {
140
+ return {
141
+ message: getWelcomeMessage(name),
142
+ options: getMainMenuKeyboard()
143
+ };
144
+ }
145
+ };
src/bots/handlers/paymentWebhookHandlers.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import { PaymentVerificationService } from '../services/PaymentVerificationService';
3
+ import { createLogger } from '../../utils/logger';
4
+
5
+ const logger = createLogger('PaymentWebhookHandlers');
6
+ const paymentVerificationService = PaymentVerificationService.getInstance();
7
+
8
+ export const setupPaymentWebhookHandlers = (bot: any) => {
9
+ // Set bot instance in verification service
10
+ paymentVerificationService.setBot(bot);
11
+
12
+ // PayPal webhook handler
13
+ bot.on('webhook:paypal', async (ctx: BotContext) => {
14
+ try {
15
+ const { paymentId, status } = ctx.webhookData;
16
+ await paymentVerificationService.verifyAndUpdatePayment(paymentId, status);
17
+ return { success: true };
18
+ } catch (error) {
19
+ logger.error('PayPal webhook error:', error);
20
+ return { success: false, error: error.message };
21
+ }
22
+ });
23
+
24
+ // Crypto payment verification handler
25
+ bot.on('webhook:crypto', async (ctx: BotContext) => {
26
+ try {
27
+ const { paymentId, status } = ctx.webhookData;
28
+ await paymentVerificationService.verifyAndUpdatePayment(paymentId, status);
29
+ return { success: true };
30
+ } catch (error) {
31
+ logger.error('Crypto webhook error:', error);
32
+ return { success: false, error: error.message };
33
+ }
34
+ });
35
+
36
+ // Admin payment verification handler
37
+ bot.on('webhook:admin', async (ctx: BotContext) => {
38
+ try {
39
+ const { paymentId, status } = ctx.webhookData;
40
+ await paymentVerificationService.verifyAndUpdatePayment(paymentId, status);
41
+ return { success: true };
42
+ } catch (error) {
43
+ logger.error('Admin webhook error:', error);
44
+ return { success: false, error: error.message };
45
+ }
46
+ });
47
+ };
src/bots/handlers/profileHandlers.ts ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { getProfileKeyboard, getLoggedInMenuKeyboard, getMainMenuKeyboard } from "../utils/keyboardUtils";
3
+ import { authService } from "../services/auth";
4
+ import { supabase } from "../../db/supabase";
5
+ import { createLogger } from "../../utils/logger";
6
+ import { messageReplyHandler } from "../utils/handlerUtils";
7
+
8
+ const logger = createLogger('ProfileHandlers');
9
+
10
+ // State management for profile actions
11
+ const profileStates = new Map<number, {
12
+ action: 'change_email' | 'change_password' | null;
13
+ step: number;
14
+ }>();
15
+
16
+ export const setupProfileHandlers = (bot: any) => {
17
+ // Profile menu handlers
18
+ bot.action('profile', async (ctx: BotContext) => {
19
+ const result = await handleProfileAction(ctx);
20
+ await ctx.editMessageText(result.message, result.options);
21
+ });
22
+
23
+ bot.action('account_info', async (ctx: BotContext) => {
24
+ const result = await handleAccountInfoAction(ctx);
25
+ await ctx.editMessageText(result.message, result.options);
26
+ });
27
+
28
+ bot.action('change_email', async (ctx: BotContext) => {
29
+ const result = await handleChangeEmailAction(ctx);
30
+ await ctx.editMessageText(result.message, result.options);
31
+ });
32
+
33
+ bot.action('change_password', async (ctx: BotContext) => {
34
+ const result = await handleChangePasswordAction(ctx);
35
+ await ctx.editMessageText(result.message, result.options);
36
+ });
37
+
38
+ // Message handlers for input processing
39
+ bot.on('text', async (ctx: BotContext) => {
40
+ const telegramId = ctx.from?.id;
41
+ if (!telegramId) return;
42
+
43
+ const userState = profileStates.get(telegramId);
44
+ if (!userState) return;
45
+
46
+ let result;
47
+ switch (userState.action) {
48
+ case 'change_email':
49
+ result = await handleEmailInput(ctx);
50
+ break;
51
+ case 'change_password':
52
+ result = await handlePasswordInput(ctx);
53
+ break;
54
+ }
55
+
56
+ if (result) {
57
+ await ctx.reply(result.message, result.options);
58
+ }
59
+ });
60
+ };
61
+
62
+ const handleProfileAction = async (ctx: BotContext) => {
63
+ const telegramId = ctx.from?.id;
64
+
65
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
66
+ return {
67
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
68
+ options: getMainMenuKeyboard()
69
+ };
70
+ }
71
+
72
+ const user = await authService.getUserByTelegramId(telegramId);
73
+
74
+ if (!user) {
75
+ return {
76
+ message: "⚠️ حدث خطأ في تحديد المستخدم",
77
+ options: getMainMenuKeyboard()
78
+ };
79
+ }
80
+
81
+ return {
82
+ message: `👤 <b>الملف الشخصي</b>\n\n` +
83
+ `اختر الإجراء الذي تريد تنفيذه:`,
84
+ options: {
85
+ ...getProfileKeyboard(),
86
+ parse_mode: 'HTML'
87
+ }
88
+ };
89
+ };
90
+
91
+ const handleAccountInfoAction = async (ctx: BotContext) => {
92
+ const telegramId = ctx.from?.id;
93
+
94
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
95
+ return {
96
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
97
+ options: getMainMenuKeyboard()
98
+ };
99
+ }
100
+
101
+ const user = await authService.getUserByTelegramId(telegramId);
102
+
103
+ if (!user) {
104
+ return {
105
+ message: "⚠️ حدث خطأ في تحديد المستخدم",
106
+ options: getMainMenuKeyboard()
107
+ };
108
+ }
109
+
110
+ return {
111
+ message: `👤 <b>معلومات الحساب</b>\n\n` +
112
+ `الاسم: ${user.firstName}\n` +
113
+ `البريد الإلكتروني: ${user.email}\n` +
114
+ `الرصيد: ${user.balance || 0}$\n` +
115
+ `تاريخ التسجيل: ${new Date(user.createdAt).toLocaleDateString()}`,
116
+ options: {
117
+ ...getProfileKeyboard(),
118
+ parse_mode: 'HTML'
119
+ }
120
+ };
121
+ };
122
+
123
+ const handleChangeEmailAction = async (ctx: BotContext) => {
124
+ const telegramId = ctx.from?.id;
125
+
126
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
127
+ return {
128
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
129
+ options: getMainMenuKeyboard()
130
+ };
131
+ }
132
+
133
+ // Set user state for email change
134
+ profileStates.set(telegramId, { action: 'change_email', step: 1 });
135
+ logger.info(`Set profile state for ${telegramId}:`, profileStates.get(telegramId));
136
+
137
+ return {
138
+ message: "📧 <b>تغيير البريد الإلكتروني</b>\n\n" +
139
+ "الرجاء إدخال البريد الإلكتروني الجديد:\n" +
140
+ "يجب أن يكون البريد الإلكتروني صالحاً ويحتوي على @",
141
+ options: {
142
+ ...getProfileKeyboard(),
143
+ parse_mode: 'HTML'
144
+ }
145
+ };
146
+ };
147
+
148
+ const handleChangePasswordAction = async (ctx: BotContext) => {
149
+ const telegramId = ctx.from?.id;
150
+
151
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
152
+ return {
153
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
154
+ options: getMainMenuKeyboard()
155
+ };
156
+ }
157
+
158
+ // Set user state for password change
159
+ profileStates.set(telegramId, { action: 'change_password', step: 1 });
160
+
161
+ return {
162
+ message: "🔑 <b>تغيير كلمة المرور</b>\n\n" +
163
+ "الرجاء إدخال كلمة المرور الجديدة:\n" +
164
+ "يجب أن تكون كلمة المرور 8 أحرف على الأقل وتحتوي على:\n" +
165
+ "- حرف كبير\n" +
166
+ "- حرف صغير\n" +
167
+ "- رقم\n" +
168
+ "- رمز خاص",
169
+ options: {
170
+ ...getProfileKeyboard(),
171
+ parse_mode: 'HTML'
172
+ }
173
+ };
174
+ };
175
+
176
+ // Input handlers
177
+ export const handleEmailInput = async (ctx: BotContext) => {
178
+ const telegramId = ctx.from?.id;
179
+
180
+ if (!telegramId || !profileStates.get(telegramId)) {
181
+ return;
182
+ }
183
+
184
+ // Use a type guard to check for text message
185
+ const message = ctx.message;
186
+ const email = (message && 'text' in message) ? message.text : undefined;
187
+ if (!email) return;
188
+
189
+ // Remove state
190
+ profileStates.delete(telegramId);
191
+
192
+ // Validate email
193
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
194
+ if (!emailRegex.test(email)) {
195
+ return {
196
+ message: "❌ البريد الإلكتروني غير صالح. الرجاء إدخال بريد إلكتروني صحيح.",
197
+ options: {
198
+ ...getProfileKeyboard(),
199
+ parse_mode: 'HTML'
200
+ }
201
+ };
202
+ }
203
+
204
+ try {
205
+ // Check if email is already in use
206
+ const { data: existingUser, error: checkError } = await supabase
207
+ .from('users_bot_telegram')
208
+ .select('id')
209
+ .eq('email', email)
210
+ .single();
211
+
212
+ if (checkError) {
213
+ logger.error(`Error checking existing email: ${checkError}`);
214
+ throw checkError;
215
+ }
216
+
217
+ if (existingUser) {
218
+ return {
219
+ message: "❌ هذا البريد الإلكتروني مستخدم بالفعل. الرجاء استخدام بريد إلكتروني آخر.",
220
+ options: {
221
+ ...getProfileKeyboard(),
222
+ parse_mode: 'HTML'
223
+ }
224
+ };
225
+ }
226
+
227
+ // Update email in database
228
+ const { error: updateError } = await supabase
229
+ .from('users_bot_telegram')
230
+ .update({ email })
231
+ .eq('telegram_id', telegramId);
232
+
233
+ if (updateError) {
234
+ logger.error(`Error updating email: ${updateError}`);
235
+ throw updateError;
236
+ }
237
+
238
+ return {
239
+ message: "✅ تم تغيير البريد الإلكتروني بنجاح!",
240
+ options: {
241
+ ...getLoggedInMenuKeyboard(),
242
+ parse_mode: 'HTML'
243
+ }
244
+ };
245
+ } catch (error) {
246
+ logger.error(`Error updating email for user ${telegramId}: ${error}`);
247
+ return {
248
+ message: "❌ حدث خطأ أثناء تحديث البريد الإلكتروني. الرجاء المحاولة مرة أخرى.",
249
+ options: {
250
+ ...getProfileKeyboard(),
251
+ parse_mode: 'HTML'
252
+ }
253
+ };
254
+ }
255
+ };
256
+
257
+ export const handlePasswordInput = async (ctx: BotContext) => {
258
+ const telegramId = ctx.from?.id;
259
+
260
+ if (!telegramId || !profileStates.get(telegramId)) {
261
+ return;
262
+ }
263
+
264
+ // Use a type guard to check for text message
265
+ const message = ctx.message;
266
+ const password = (message && 'text' in message) ? message.text : undefined;
267
+ if (!password) return;
268
+
269
+ // Remove state
270
+ profileStates.delete(telegramId);
271
+
272
+ // Validate password
273
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
274
+ if (!passwordRegex.test(password)) {
275
+ return {
276
+ message: "❌ كلمة المرور غير صالحة. الرجاء التأكد من أن كلمة المرور:\n" +
277
+ "- 8 أحرف على الأقل\n" +
278
+ "- تحتوي على حرف كبير\n" +
279
+ "- تحتوي على حرف صغير\n" +
280
+ "- تحتوي على رقم\n" +
281
+ "- تحتوي على رمز خاص",
282
+ options: {
283
+ ...getProfileKeyboard(),
284
+ parse_mode: 'HTML'
285
+ }
286
+ };
287
+ }
288
+
289
+ try {
290
+ // Hash the new password
291
+ const passwordHash = await authService.hashPassword(password);
292
+
293
+ // Update password in database
294
+ const { error } = await supabase
295
+ .from('users_bot_telegram')
296
+ .update({ passwordHash })
297
+ .eq('telegram_id', telegramId);
298
+
299
+ if (error) throw error;
300
+
301
+ return {
302
+ message: "✅ تم تغيير كلمة المرور بنجاح!",
303
+ options: {
304
+ ...getLoggedInMenuKeyboard(),
305
+ parse_mode: 'HTML'
306
+ }
307
+ };
308
+ } catch (error) {
309
+ logger.error(`Error updating password for user ${telegramId}: ${error}`);
310
+ return {
311
+ message: "❌ حدث خطأ أثناء تحديث كلمة المرور. الرجاء المحاولة مرة أخرى.",
312
+ options: {
313
+ ...getProfileKeyboard(),
314
+ parse_mode: 'HTML'
315
+ }
316
+ };
317
+ }
318
+ };
src/bots/handlers/purchaseHandlers.ts ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { AuthService } from '../services/auth';
3
+ import { VirtualNumberService } from '../services/VirtualNumberService';
4
+ import { PurchaseTrackingService, PurchaseState } from '../services/PurchaseTrackingService';
5
+ import { createLogger } from '../../utils/logger';
6
+ import { serviceMapping } from './serviceHandlers';
7
+ import {
8
+ getAuthRequiredMessage,
9
+ getServiceUnavailableMessage,
10
+ getInsufficientBalanceMessage,
11
+ getPurchaseSuccessMessage,
12
+ getPurchaseFailedMessage,
13
+ getPurchaseErrorMessage,
14
+ getVerificationTimeoutMessage,
15
+ getProcessingPurchaseMessage
16
+ } from "../utils/messageUtils";
17
+ import { getLoggedInMenuKeyboard, getMainMenuKeyboard } from "../utils/keyboardUtils";
18
+ import { messageManager } from "../utils/messageManager";
19
+
20
+ const logger = createLogger('PurchaseHandlers');
21
+ const authService = AuthService.getInstance();
22
+ const virtualNumberService = VirtualNumberService.getInstance();
23
+ const purchaseTrackingService = PurchaseTrackingService.getInstance();
24
+
25
+ export const setupPurchaseHandlers = (bot: any) => {
26
+ // Register purchase handler
27
+ bot.action(/^buy_(.+)_(.+)_(.+)$/, async (ctx: BotContext) => {
28
+ await ctx.answerCbQuery();
29
+
30
+ const match = ctx.match as RegExpMatchArray;
31
+ const service = match[1];
32
+ const countryId = match[2];
33
+ const operator = match[3];
34
+
35
+ // First send the processing message
36
+ const loadingMsg = await ctx.reply(getProcessingPurchaseMessage(service, countryId));
37
+
38
+ // Then execute the actual purchase logic
39
+ try {
40
+ await executePurchase(ctx, service, countryId, operator, serviceMapping, loadingMsg.message_id);
41
+ } catch (error: any) {
42
+ logger.error(`Error in purchase execution: ${error.message}`);
43
+ await ctx.deleteMessage(loadingMsg.message_id);
44
+ await ctx.reply(getPurchaseErrorMessage(error.message), getLoggedInMenuKeyboard());
45
+ }
46
+ });
47
+ };
48
+
49
+ export const executePurchase = async (
50
+ ctx: BotContext,
51
+ service: string,
52
+ countryId: string,
53
+ operator: string,
54
+ serviceMapping: {[key: string]: string},
55
+ loadingMsgId: number
56
+ ) => {
57
+ // 🔒 Authentication check
58
+ const telegramId = ctx.from?.id;
59
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
60
+ await ctx.deleteMessage(loadingMsgId);
61
+ await ctx.reply(getAuthRequiredMessage(), getMainMenuKeyboard());
62
+ return;
63
+ }
64
+
65
+ try {
66
+ const fiveSimService = serviceMapping[service];
67
+ const prices = await virtualNumberService.getPrices(fiveSimService, countryId);
68
+
69
+ // Add validation to check if the price exists for this operator
70
+ if (!prices || !prices[countryId] || !prices[countryId][fiveSimService] || !prices[countryId][fiveSimService][operator]) {
71
+ await ctx.deleteMessage(loadingMsgId);
72
+ await ctx.reply(
73
+ getServiceUnavailableMessage(service, operator, countryId),
74
+ getLoggedInMenuKeyboard()
75
+ );
76
+ return;
77
+ }
78
+
79
+ const cost = prices[countryId][fiveSimService][operator].cost;
80
+
81
+ // 💰 Balance check
82
+ const user = await authService.getUserByTelegramId(telegramId);
83
+ if (!user || user.balance < cost) {
84
+ await ctx.deleteMessage(loadingMsgId);
85
+ await ctx.reply(
86
+ getInsufficientBalanceMessage(cost, user?.balance || 0),
87
+ getLoggedInMenuKeyboard()
88
+ );
89
+ return;
90
+ }
91
+
92
+ // 🛒 Purchase number
93
+ const purchaseResult = await virtualNumberService.purchaseNumber(fiveSimService, countryId, operator);
94
+ await ctx.deleteMessage(loadingMsgId);
95
+
96
+ if (purchaseResult && purchaseResult.phone) {
97
+ // ➖ Deduct balance
98
+ await authService.updateUserBalance(telegramId, user.balance - cost);
99
+
100
+ // 💾 Create purchase record in database with PENDING state
101
+ const purchaseRecord = await purchaseTrackingService.createPurchase({
102
+ userId: user.id,
103
+ telegramId: telegramId,
104
+ service: service,
105
+ countryId: countryId,
106
+ operator: operator,
107
+ phoneNumber: purchaseResult.phone,
108
+ orderId: purchaseResult.id,
109
+ cost: cost,
110
+ state: PurchaseState.PENDING
111
+ });
112
+
113
+ // Send purchase confirmation
114
+ const confirmMsg = await ctx.reply(
115
+ getPurchaseSuccessMessage(purchaseResult, service, countryId, operator, cost),
116
+ getLoggedInMenuKeyboard()
117
+ );
118
+
119
+ // Start checking for verification code
120
+ const orderId = purchaseResult.id;
121
+ const maxAttempts = 30; // Check for up to 15 minutes (30 × 30s = 15min)
122
+ let attempts = 0;
123
+
124
+ const checkInterval = setInterval(async () => {
125
+ try {
126
+ attempts++;
127
+
128
+ // Check if we've reached max attempts
129
+ if (attempts >= maxAttempts) {
130
+ clearInterval(checkInterval);
131
+
132
+ // Update purchase record to TIMEOUT state
133
+ await purchaseTrackingService.updatePurchaseState(
134
+ orderId,
135
+ PurchaseState.TIMEOUT
136
+ );
137
+
138
+ await ctx.reply(
139
+ getVerificationTimeoutMessage(),
140
+ getLoggedInMenuKeyboard()
141
+ );
142
+ return;
143
+ }
144
+
145
+ // Check for SMS
146
+ const smsResult = await virtualNumberService.checkSMS(orderId);
147
+
148
+ // If SMS received
149
+ if (smsResult && smsResult.sms && smsResult.sms.length > 0) {
150
+ clearInterval(checkInterval);
151
+
152
+ // Get the latest SMS
153
+ const latestSMS = smsResult.sms[smsResult.sms.length - 1];
154
+
155
+ // Extract verification code (this is a simple pattern, might need adjustment)
156
+ let verificationCode = latestSMS.text;
157
+
158
+ // Try to extract numeric code if it exists
159
+ const codeMatch = verificationCode.match(/\b\d{4,8}\b/);
160
+ if (codeMatch) {
161
+ verificationCode = `${codeMatch[0]} (${verificationCode})`;
162
+ }
163
+
164
+ // Create verification message
165
+ const verificationMessage = messageManager.getMessage('verification_received')
166
+ .replace('{phone}', purchaseResult.phone)
167
+ .replace('{code}', verificationCode)
168
+ .replace('{time}', new Date().toLocaleTimeString('ar-SA'));
169
+
170
+ // Update purchase record to SUCCESS state
171
+ await purchaseTrackingService.updatePurchaseState(
172
+ orderId,
173
+ PurchaseState.SUCCESS,
174
+ {
175
+ verificationCode: verificationCode,
176
+ verificationMessage: verificationMessage
177
+ }
178
+ );
179
+
180
+ await ctx.reply(
181
+ verificationMessage,
182
+ getLoggedInMenuKeyboard()
183
+ );
184
+ }
185
+ } catch (error: any) {
186
+ logger.error(`Error checking SMS for order ${orderId}: ${error.message}`);
187
+ // Don't stop checking on error, just log it
188
+ }
189
+ }, 30000); // Check every 30 seconds
190
+
191
+ } else {
192
+ // Create purchase record with FAILED state
193
+ if (user) {
194
+ await purchaseTrackingService.createPurchase({
195
+ userId: user.id,
196
+ telegramId: telegramId,
197
+ service: service,
198
+ countryId: countryId,
199
+ operator: operator,
200
+ cost: cost,
201
+ state: PurchaseState.FAILED
202
+ });
203
+ }
204
+
205
+ await ctx.reply(getPurchaseFailedMessage(), getLoggedInMenuKeyboard());
206
+ }
207
+
208
+ } catch (error: any) {
209
+ logger.error(`Error purchasing number for ${service} in ${countryId}: ${error.message}`);
210
+
211
+ // Create purchase record with FAILED state if we have user info
212
+ try {
213
+ const user = await authService.getUserByTelegramId(telegramId!);
214
+ if (user) {
215
+ await purchaseTrackingService.createPurchase({
216
+ userId: user.id,
217
+ telegramId: telegramId!,
218
+ service: service,
219
+ countryId: countryId,
220
+ operator: operator,
221
+ cost: 0, // We don't know the cost in this error case
222
+ state: PurchaseState.FAILED
223
+ });
224
+ }
225
+ } catch (dbError) {
226
+ logger.error(`Error creating failed purchase record: ${dbError}`);
227
+ }
228
+
229
+ await ctx.deleteMessage(loadingMsgId);
230
+ await ctx.reply(getPurchaseErrorMessage(error.message), getLoggedInMenuKeyboard());
231
+ }
232
+ };
src/bots/handlers/serviceHandlers.ts ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { AuthService } from '../services/auth';
3
+ import { VirtualNumberService } from '../services/VirtualNumberService';
4
+ import { createLogger } from '../../utils/logger';
5
+ import { callbackReplyHandler } from "../utils/handlerUtils";
6
+ import {
7
+ getAuthRequiredMessage,
8
+ getCountriesListMessage,
9
+ getServiceErrorMessage,
10
+ getNoPricesMessage,
11
+ getPricesListMessage,
12
+ getPricesErrorMessage
13
+ } from "../utils/messageUtils";
14
+ import {
15
+ getLoggedInMenuKeyboard,
16
+ getMainMenuKeyboard,
17
+ getCountriesKeyboard,
18
+ getServicePricesKeyboard
19
+ } from "../utils/keyboardUtils";
20
+ import { calculatePriceWithProfit, formatPrice } from "../utils/priceUtils";
21
+
22
+ const logger = createLogger('ServiceHandlers');
23
+ const authService = AuthService.getInstance();
24
+ const virtualNumberService = VirtualNumberService.getInstance();
25
+
26
+ // Service name to API code mapping
27
+ export const serviceMapping: {[key: string]: string} = {
28
+ 'telegram': 'telegram',
29
+ 'whatsapp': 'whatsapp',
30
+ 'facebook': 'facebook',
31
+ 'instagram': 'instagram',
32
+ 'tiktok': 'tiktok',
33
+ 'twitter': 'twitter',
34
+ 'snapchat': 'snapchat',
35
+ 'google': 'google',
36
+ 'imo': 'imo',
37
+ 'general': 'other',
38
+ 'royal': 'other',
39
+ 'random': 'other'
40
+ };
41
+
42
+ export const setupServiceHandlers = (bot: any) => {
43
+ // 🌐 Service configuration
44
+ const services = [
45
+ 'telegram', 'whatsapp', 'facebook', 'instagram',
46
+ 'tiktok', 'twitter', 'snapchat', 'google',
47
+ 'imo', 'general', 'royal', 'random'
48
+ ];
49
+
50
+ // Register service selection handlers
51
+ services.forEach(service => {
52
+ bot.action(`service_${service}`, callbackReplyHandler(async (ctx: BotContext) => {
53
+ return handleServiceSelection(ctx, service, serviceMapping);
54
+ }));
55
+ });
56
+
57
+ // Register country selection handler
58
+ bot.action(/^country_(.+)_(.+)$/, callbackReplyHandler(async (ctx: BotContext) => {
59
+ return handleCountrySelection(ctx, serviceMapping);
60
+ }));
61
+
62
+ // Register pagination handler
63
+ bot.action(/^page_(.+)_(\d+)$/, async (ctx: BotContext) => {
64
+ await ctx.answerCbQuery();
65
+
66
+ const match = ctx.match as RegExpMatchArray;
67
+ const service = match[1];
68
+ const page = parseInt(match[2], 10);
69
+
70
+ // 🔒 Authentication check
71
+ const telegramId = ctx.from?.id;
72
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
73
+ await ctx.reply(getAuthRequiredMessage(), getMainMenuKeyboard());
74
+ return;
75
+ }
76
+
77
+ try {
78
+ // Edit the existing message instead of sending a new one
79
+ await (ctx as any).editMessageText(
80
+ getCountriesListMessage(service, page),
81
+ getCountriesKeyboard(service, page)
82
+ );
83
+ } catch (error: any) {
84
+ logger.error(`Error navigating to page ${page} for ${service}: ${error.message}`);
85
+ await ctx.reply(getPaginationErrorMessage(), getLoggedInMenuKeyboard());
86
+ }
87
+ });
88
+ };
89
+
90
+ // Helper function for pagination error message
91
+ const getPaginationErrorMessage = () => {
92
+ return `⚠️ حدث خطأ أثناء تحميل الصفحة. يرجى المحاولة مرة أخرى.`;
93
+ };
94
+
95
+ export const handleServiceSelection = async (ctx: BotContext, service: string, serviceMapping: {[key: string]: string}) => {
96
+ // 🔒 Authentication check
97
+ const telegramId = ctx.from?.id;
98
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
99
+ return {
100
+ message: getAuthRequiredMessage(),
101
+ options: getMainMenuKeyboard()
102
+ };
103
+ }
104
+
105
+ try {
106
+ return {
107
+ message: getCountriesListMessage(service),
108
+ options: getCountriesKeyboard(service, 0)
109
+ };
110
+
111
+ } catch (error: any) {
112
+ logger.error(`Error fetching countries for ${service}: ${error.message}`);
113
+ return {
114
+ message: getServiceErrorMessage(service),
115
+ options: getLoggedInMenuKeyboard()
116
+ };
117
+ }
118
+ };
119
+
120
+
121
+
122
+ export const handleCountrySelection = async (ctx: BotContext, serviceMapping: {[key: string]: string}) => {
123
+ const match = ctx.match as RegExpMatchArray;
124
+ const service = match[1];
125
+ const countryId = match[2];
126
+
127
+ // Authentication check
128
+ const telegramId = ctx.from?.id;
129
+ if (!telegramId || !authService.isUserLoggedIn(telegramId)) {
130
+ return {
131
+ message: getAuthRequiredMessage(),
132
+ options: getMainMenuKeyboard()
133
+ };
134
+ }
135
+
136
+ try {
137
+ const fiveSimService = serviceMapping[service];
138
+ const prices = await virtualNumberService.getPrices(fiveSimService, countryId);
139
+ // ctx.reply(`prices: ${JSON.stringify(prices, null, 2)}`);
140
+
141
+ if (!prices || !prices[countryId] || !prices[countryId][fiveSimService] ||
142
+ Object.keys(prices[countryId][fiveSimService]).length === 0) {
143
+ return {
144
+ message: getNoPricesMessage(service, countryId),
145
+ options: getLoggedInMenuKeyboard()
146
+ };
147
+ }
148
+
149
+ // Apply profit to prices before displaying
150
+ const pricesWithProfit = Object.entries(prices[countryId][fiveSimService]).reduce((acc: any, [operator, operatorData]) => {
151
+ // operatorData contains cost and count properties
152
+ if (operatorData && typeof operatorData === 'object' && 'cost' in operatorData) {
153
+ acc[operator] = {
154
+ ...operatorData,
155
+ cost: calculatePriceWithProfit(ctx, Number(operatorData.cost), 'RUB')
156
+ };
157
+ }
158
+ return acc;
159
+ }, {});
160
+
161
+ return {
162
+ message: getPricesListMessage(service, countryId, pricesWithProfit, ctx),
163
+ options: getServicePricesKeyboard(pricesWithProfit, service, countryId, ctx)
164
+ };
165
+
166
+ } catch (error: any) {
167
+ logger.error(`Error fetching prices for ${service} in ${countryId}: ${error.message}`);
168
+
169
+ return {
170
+ message: getPricesErrorMessage(service, countryId),
171
+ options: getLoggedInMenuKeyboard()
172
+ };
173
+ }
174
+ };
src/bots/index.ts ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initializeBot } from "./botManager";
2
+ import { telegrafBots } from "../models";
3
+ import { supabase } from "../db/supabase";
4
+ import { createLogger } from "../utils/logger";
5
+ import { setupPaymentWebhookHandlers } from './handlers/paymentWebhookHandlers';
6
+
7
+ const logger = createLogger('BotManager');
8
+
9
+ export const handleAddTelegrafBot = async (botId: string) => {
10
+ if (!botId) {
11
+ return { status: 400, data: { error: "Bot ID is required" } };
12
+ }
13
+
14
+ try {
15
+ logger.info(`Fetching bot data for bot ID: ${botId}`);
16
+ // Fetch bot data from database
17
+ const { data: botData, error } = await supabase
18
+ .from('bots')
19
+ .select('*')
20
+ .eq('id', botId)
21
+ .single();
22
+
23
+ if (error) {
24
+ logger.error(`Error fetching bot data: ${error.message}`);
25
+ return { status: 404, data: { error: `Bot not found: ${error.message}` } };
26
+ }
27
+
28
+ if (!botData || !botData.bot_token) {
29
+ return { status: 404, data: { error: "Bot token not found in database" } };
30
+ }
31
+
32
+ // Check if bot is already running
33
+ if (telegrafBots.has(botData.bot_token)) {
34
+ return { status: 400, data: { error: "Bot is already running" } };
35
+ }
36
+
37
+ // Validate bot token format
38
+ if (!botData.bot_token.includes(':')) {
39
+ logger.error(`Invalid bot token format for bot ${botId}`);
40
+ return { status: 400, data: { error: "Invalid bot token format" } };
41
+ }
42
+
43
+ try {
44
+ // Initialize the bot with settings from database
45
+ const result = await initializeBot(botData.bot_token, botData);
46
+
47
+ if (result.success && result.bot) {
48
+ // Store the bot instance in the map
49
+ telegrafBots.set(botData.bot_token, result.bot);
50
+
51
+ try {
52
+ // Launch the bot
53
+ // await result.bot.launch();
54
+
55
+ // Update last_activity and is_active in database
56
+ await supabase
57
+ .from('bots')
58
+ .update({
59
+ is_active: true,
60
+ last_activity: new Date().toISOString(),
61
+ state: {
62
+ status: 'running',
63
+ startedAt: new Date().toISOString()
64
+ }
65
+ })
66
+ .eq('id', botId);
67
+
68
+ logger.info(`Bot ${botData.name} (${botId}) launched successfully`);
69
+
70
+ await result.bot.launch();
71
+
72
+ // Setup payment webhook handlers
73
+ setupPaymentWebhookHandlers(result.bot);
74
+
75
+ return { status: 200, data: {
76
+ message: result.message,
77
+ bot: {
78
+ id: botData.id,
79
+ name: botData.name,
80
+ is_active: true
81
+ }
82
+ }};
83
+
84
+ } catch (error: any) {
85
+ logger.error(`Error launching bot: ${error.message}`);
86
+ return { status: 500, data: { error: `Error launching bot: ${error.message}` } };
87
+ }
88
+ } else {
89
+ logger.error(`Error initializing bot: ${result.message}`);
90
+ return { status: 500, data: { error: result.message } };
91
+ }
92
+ } catch (error: any) {
93
+ logger.error(`Bot initialization error: ${error.message}`);
94
+ return { status: 500, data: { error: `Bot initialization error: ${error.message}` } };
95
+ }
96
+ } catch (error: any) {
97
+ logger.error(`Unexpected error: ${error.message}`);
98
+ return { status: 500, data: { error: `Unexpected error: ${error.message}` } };
99
+ }
100
+ };
src/bots/services/AdminService.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLogger } from '../../utils/logger';
2
+ import { supabase } from '../../db/supabase';
3
+
4
+ const logger = createLogger('AdminService');
5
+
6
+ export class AdminService {
7
+ private static instance: AdminService;
8
+ private readonly adminContact: string;
9
+
10
+ private constructor() {
11
+ this.adminContact = process.env.ADMIN_CONTACT || '';
12
+ }
13
+
14
+ public static getInstance(): AdminService {
15
+ if (!AdminService.instance) {
16
+ AdminService.instance = new AdminService();
17
+ }
18
+ return AdminService.instance;
19
+ }
20
+
21
+ async getAdminContact(): Promise<string> {
22
+ return this.adminContact;
23
+ }
24
+ }
src/bots/services/CryptoService.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLogger } from '../../utils/logger';
2
+ import { supabase } from '../../db/supabase';
3
+
4
+ const logger = createLogger('CryptoService');
5
+
6
+ export class CryptoService {
7
+ private static instance: CryptoService;
8
+ private readonly cryptoAddress: string;
9
+
10
+ private constructor() {
11
+ this.cryptoAddress = process.env.CRYPTO_WALLET_ADDRESS || '';
12
+ }
13
+
14
+ public static getInstance(): CryptoService {
15
+ if (!CryptoService.instance) {
16
+ CryptoService.instance = new CryptoService();
17
+ }
18
+ return CryptoService.instance;
19
+ }
20
+
21
+ async getPaymentAddress(userId: string, amount: number): Promise<string> {
22
+ try {
23
+ // Create payment record in database
24
+ const { data: payment, error } = await supabase
25
+ .from('payments')
26
+ .insert({
27
+ user_id: userId,
28
+ amount: amount,
29
+ status: 'PENDING',
30
+ payment_method: 'CRYPTO'
31
+ })
32
+ .select()
33
+ .single();
34
+
35
+ if (error) throw error;
36
+
37
+ return this.cryptoAddress;
38
+ } catch (error) {
39
+ logger.error('Error creating crypto payment:', error);
40
+ throw error;
41
+ }
42
+ }
43
+ }
src/bots/services/PayPalService.ts ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLogger } from '../../utils/logger';
2
+ import { supabase } from '../../db/supabase';
3
+
4
+ const logger = createLogger('PayPalService');
5
+
6
+ // PayPal API Response Types
7
+ interface PayPalLink {
8
+ href: string;
9
+ rel: string;
10
+ method: string;
11
+ }
12
+
13
+ interface PayPalOrder {
14
+ id: string;
15
+ status: string;
16
+ links: PayPalLink[];
17
+ }
18
+
19
+ interface PayPalTokenResponse {
20
+ access_token: string;
21
+ token_type: string;
22
+ expires_in: number;
23
+ }
24
+
25
+ export class PayPalService {
26
+ private static instance: PayPalService;
27
+ private readonly clientId: string;
28
+ private readonly clientSecret: string;
29
+ private readonly isProduction: boolean;
30
+
31
+ private constructor() {
32
+ this.clientId = process.env.PAYPAL_CLIENT_ID || '';
33
+ this.clientSecret = process.env.PAYPAL_CLIENT_SECRET || '';
34
+ this.isProduction = process.env.NODE_ENV === 'production';
35
+ }
36
+
37
+ public static getInstance(): PayPalService {
38
+ if (!PayPalService.instance) {
39
+ PayPalService.instance = new PayPalService();
40
+ }
41
+ return PayPalService.instance;
42
+ }
43
+
44
+ async createPaymentLink(userId: string, amount: number): Promise<string> {
45
+ try {
46
+ // Create payment record in database
47
+ const { data: payment, error } = await supabase
48
+ .from('payments')
49
+ .insert({
50
+ user_id: userId,
51
+ amount: amount,
52
+ status: 'PENDING',
53
+ payment_method: 'PAYPAL'
54
+ })
55
+ .select()
56
+ .single();
57
+
58
+ if (error) throw error;
59
+
60
+ // Create PayPal order
61
+ const order = await this.createPayPalOrder(amount, payment.id);
62
+
63
+ // Update payment record with PayPal order ID
64
+ await supabase
65
+ .from('payments')
66
+ .update({ payment_id: order.id })
67
+ .eq('id', payment.id);
68
+
69
+ const approveLink = order.links.find(link => link.rel === 'approve');
70
+ if (!approveLink) {
71
+ throw new Error('No approval link found in PayPal response');
72
+ }
73
+
74
+ return approveLink.href;
75
+ } catch (error) {
76
+ logger.error('Error creating PayPal payment:', error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ private async createPayPalOrder(amount: number, paymentId: string): Promise<PayPalOrder> {
82
+ const accessToken = await this.getAccessToken();
83
+
84
+ const response = await fetch(
85
+ `${this.isProduction ? 'https://api-m.paypal.com' : 'https://api-m.sandbox.paypal.com'}/v2/checkout/orders`,
86
+ {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ 'Authorization': `Bearer ${accessToken}`
91
+ },
92
+ body: JSON.stringify({
93
+ intent: 'CAPTURE',
94
+ purchase_units: [{
95
+ amount: {
96
+ currency_code: 'USD',
97
+ value: amount.toString()
98
+ },
99
+ custom_id: paymentId
100
+ }]
101
+ })
102
+ }
103
+ );
104
+
105
+ if (!response.ok) {
106
+ throw new Error(`PayPal API error: ${response.statusText}`);
107
+ }
108
+
109
+ const data = await response.json() as PayPalOrder;
110
+ return data;
111
+ }
112
+
113
+ private async getAccessToken(): Promise<string> {
114
+ const response = await fetch(
115
+ `${this.isProduction ? 'https://api-m.paypal.com' : 'https://api-m.sandbox.paypal.com'}/v1/oauth2/token`,
116
+ {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/x-www-form-urlencoded',
120
+ 'Authorization': `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`
121
+ },
122
+ body: 'grant_type=client_credentials'
123
+ }
124
+ );
125
+
126
+ if (!response.ok) {
127
+ throw new Error(`PayPal token error: ${response.statusText}`);
128
+ }
129
+
130
+ const data = await response.json() as PayPalTokenResponse;
131
+ return data.access_token;
132
+ }
133
+ }
src/bots/services/PaymentVerificationService.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLogger } from '../../utils/logger';
2
+ import { supabase } from '../../db/supabase';
3
+ import { AuthService } from './auth';
4
+ import { Telegraf } from 'telegraf';
5
+
6
+ const logger = createLogger('PaymentVerificationService');
7
+
8
+ export class PaymentVerificationService {
9
+ private static instance: PaymentVerificationService;
10
+ private bot: Telegraf;
11
+
12
+ private constructor() {}
13
+
14
+ public static getInstance(): PaymentVerificationService {
15
+ if (!PaymentVerificationService.instance) {
16
+ PaymentVerificationService.instance = new PaymentVerificationService();
17
+ }
18
+ return PaymentVerificationService.instance;
19
+ }
20
+
21
+ public setBot(bot: Telegraf) {
22
+ this.bot = bot;
23
+ }
24
+
25
+ async verifyAndUpdatePayment(paymentId: string, status: 'COMPLETED' | 'FAILED') {
26
+ try {
27
+ // Get payment details
28
+ const { data: payment, error: paymentError } = await supabase
29
+ .from('payments')
30
+ .select('*, users_bot_telegram!inner(telegram_id, balance)')
31
+ .eq('id', paymentId)
32
+ .single();
33
+
34
+ if (paymentError) throw paymentError;
35
+ if (!payment) throw new Error('Payment not found');
36
+
37
+ // Update payment status
38
+ const { error: updateError } = await supabase
39
+ .from('payments')
40
+ .update({
41
+ status: status,
42
+ completed_at: status === 'COMPLETED' ? new Date().toISOString() : null
43
+ })
44
+ .eq('id', paymentId);
45
+
46
+ if (updateError) throw updateError;
47
+
48
+ if (status === 'COMPLETED') {
49
+ // Update user balance
50
+ const { error: balanceError } = await supabase
51
+ .from('users_bot_telegram')
52
+ .update({
53
+ balance: payment.users_bot_telegram.balance + payment.amount
54
+ })
55
+ .eq('id', payment.user_id);
56
+
57
+ if (balanceError) throw balanceError;
58
+
59
+ // Notify user
60
+ await this.notifyUser(payment.users_bot_telegram.telegram_id, payment.amount);
61
+ }
62
+
63
+ return true;
64
+ } catch (error) {
65
+ logger.error('Error verifying payment:', error);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ private async notifyUser(telegramId: number, amount: number) {
71
+ try {
72
+ await this.bot.telegram.sendMessage(
73
+ telegramId,
74
+ `✅ <b>تم إتمام عملية الدفع بنجاح!</b>\n\n` +
75
+ `تم إضافة ${amount}$ إلى رصيدك.\n` +
76
+ `يمكنك الآن استخدام الخدمات المتاحة.`,
77
+ { parse_mode: 'HTML' }
78
+ );
79
+ } catch (error) {
80
+ logger.error('Error notifying user:', error);
81
+ }
82
+ }
83
+ }
src/bots/services/PurchaseTrackingService.ts ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLogger } from '../../utils/logger';
2
+ import { supabase } from '../../db/supabase';
3
+ import { camelToSnake, snakeToCamel } from '../../utils';
4
+
5
+ const logger = createLogger('PurchaseTrackingService');
6
+
7
+ export enum PurchaseState {
8
+ PENDING = 'pending',
9
+ SUCCESS = 'success',
10
+ FAILED = 'failed',
11
+ CANCELED = 'canceled',
12
+ TIMEOUT = 'timeout'
13
+ }
14
+
15
+ export interface PurchaseRecord {
16
+ id?: number;
17
+ userId: number;
18
+ telegramId: number;
19
+ service: string;
20
+ countryId: string;
21
+ operator: string;
22
+ phoneNumber?: string;
23
+ orderId?: string;
24
+ cost: number;
25
+ state: PurchaseState;
26
+ verificationCode?: string;
27
+ verificationMessage?: string;
28
+ createdAt?: string;
29
+ updatedAt?: string;
30
+ }
31
+
32
+ export class PurchaseTrackingService {
33
+ private static instance: PurchaseTrackingService;
34
+
35
+ private constructor() {}
36
+
37
+ public static getInstance(): PurchaseTrackingService {
38
+ if (!PurchaseTrackingService.instance) {
39
+ PurchaseTrackingService.instance = new PurchaseTrackingService();
40
+ }
41
+ return PurchaseTrackingService.instance;
42
+ }
43
+
44
+ /**
45
+ * Create a new purchase record
46
+ */
47
+ async createPurchase(purchaseData: Omit<PurchaseRecord, 'id' | 'createdAt' | 'updatedAt'>): Promise<PurchaseRecord> {
48
+ try {
49
+ // Convert camelCase to snake_case for database
50
+ const snakeData = camelToSnake(purchaseData);
51
+
52
+ const { data, error } = await supabase
53
+ .from('purchases')
54
+ .insert(snakeData)
55
+ .select();
56
+
57
+ if (error) {
58
+ logger.error(`Error creating purchase record: ${error.message}`);
59
+ throw new Error(`Failed to create purchase record: ${error.message}`);
60
+ }
61
+
62
+ // Convert snake_case back to camelCase
63
+ return snakeToCamel(data[0]) as PurchaseRecord;
64
+ } catch (error: any) {
65
+ logger.error(`Error in createPurchase: ${error.message}`);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Update purchase state and related information
72
+ */
73
+ async updatePurchaseState(
74
+ orderId: string,
75
+ state: PurchaseState,
76
+ additionalData: Partial<PurchaseRecord> = {}
77
+ ): Promise<PurchaseRecord> {
78
+ try {
79
+ const updateData = camelToSnake({
80
+ state,
81
+ updatedAt: new Date().toISOString(),
82
+ ...additionalData
83
+ });
84
+
85
+ const { data, error } = await supabase
86
+ .from('purchases')
87
+ .update(updateData)
88
+ .eq('order_id', orderId)
89
+ .select();
90
+
91
+ if (error) {
92
+ logger.error(`Error updating purchase state: ${error.message}`);
93
+ throw new Error(`Failed to update purchase state: ${error.message}`);
94
+ }
95
+
96
+ return data && data.length > 0 ? snakeToCamel(data[0]) as PurchaseRecord : {} as PurchaseRecord;
97
+ } catch (error: any) {
98
+ logger.error(`Error in updatePurchaseState: ${error.message}`);
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get purchase by order ID
105
+ */
106
+ async getPurchaseByOrderId(orderId: string): Promise<PurchaseRecord | null> {
107
+ try {
108
+ const { data, error } = await supabase
109
+ .from('purchases')
110
+ .select('*')
111
+ .eq('order_id', orderId)
112
+ .limit(1);
113
+
114
+ if (error) {
115
+ logger.error(`Error fetching purchase: ${error.message}`);
116
+ throw new Error(`Failed to fetch purchase: ${error.message}`);
117
+ }
118
+
119
+ return data && data.length > 0 ? snakeToCamel(data[0]) as PurchaseRecord : null;
120
+ } catch (error: any) {
121
+ logger.error(`Error in getPurchaseByOrderId: ${error.message}`);
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get user's purchase history
128
+ */
129
+ async getUserPurchases(telegramId: number, limit: number = 10, offset: number = 0): Promise<PurchaseRecord[]> {
130
+ try {
131
+ const { data, error } = await supabase
132
+ .from('purchases')
133
+ .select('*')
134
+ .eq('telegram_id', telegramId)
135
+ .order('created_at', { ascending: false })
136
+ .range(offset, offset + limit - 1);
137
+
138
+ if (error) {
139
+ logger.error(`Error fetching user purchases: ${error.message}`);
140
+ throw new Error(`Failed to fetch user purchases: ${error.message}`);
141
+ }
142
+
143
+ return data ? data.map(record => snakeToCamel(record) as PurchaseRecord) : [];
144
+ } catch (error: any) {
145
+ logger.error(`Error in getUserPurchases: ${error.message}`);
146
+ throw error;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get user's purchase history filtered by state
152
+ */
153
+ async getUserPurchasesByState(telegramId: number, state: PurchaseState, limit: number = 10, offset: number = 0): Promise<PurchaseRecord[]> {
154
+ try {
155
+ const { data, error } = await supabase
156
+ .from('purchases')
157
+ .select('*')
158
+ .eq('telegram_id', telegramId)
159
+ .eq('state', state)
160
+ .order('created_at', { ascending: false })
161
+ .range(offset, offset + limit - 1);
162
+
163
+ if (error) {
164
+ logger.error(`Error fetching user purchases by state: ${error.message}`);
165
+ throw new Error(`Failed to fetch user purchases by state: ${error.message}`);
166
+ }
167
+
168
+ return data ? data.map(record => snakeToCamel(record) as PurchaseRecord) : [];
169
+ } catch (error: any) {
170
+ logger.error(`Error in getUserPurchasesByState: ${error.message}`);
171
+ throw error;
172
+ }
173
+ }
174
+ }
src/bots/services/VirtualNumberService.ts ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import { createLogger } from '../../utils/logger';
3
+
4
+ const logger = createLogger('VirtualNumberService');
5
+
6
+ export interface Country {
7
+ id: string;
8
+ name: string;
9
+ count: number;
10
+ }
11
+
12
+ export interface PriceInfo {
13
+ cost: number;
14
+ count: number;
15
+ }
16
+
17
+ export interface ServicePrice {
18
+ [country: string]: {
19
+ [operator: string]: PriceInfo;
20
+ };
21
+ }
22
+
23
+ export class VirtualNumberService {
24
+ private static instance: VirtualNumberService;
25
+ private apiKey: string;
26
+ private baseUrl: string = 'https://5sim.net/v1';
27
+
28
+ private constructor() {
29
+ this.apiKey = process.env.FIVESIM_API_KEY || '';
30
+ if (!this.apiKey) {
31
+ logger.error('5sim API key not found in environment variables');
32
+ }
33
+ }
34
+
35
+ public static getInstance(): VirtualNumberService {
36
+ if (!VirtualNumberService.instance) {
37
+ VirtualNumberService.instance = new VirtualNumberService();
38
+ }
39
+ return VirtualNumberService.instance;
40
+ }
41
+
42
+ /**
43
+ * Get available countries for a specific service
44
+ */
45
+ async getAvailableCountries(service: string): Promise<Country[]> {
46
+ try {
47
+ const response = await axios.get(`${this.baseUrl}/guest/countries/${service}`, {
48
+ headers: {
49
+ 'Authorization': `Bearer ${this.apiKey}`,
50
+ 'Accept': 'application/json',
51
+ }
52
+ });
53
+
54
+ return response.data;
55
+ } catch (error: any) {
56
+ logger.error(`Error fetching countries for ${service}: ${error.message}`);
57
+ throw new Error(`Failed to fetch available countries: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get prices for a specific service and country
63
+ */
64
+ async getPrices(service: string, country: string): Promise<ServicePrice> {
65
+ try {
66
+ const response = await axios.get(`${this.baseUrl}/guest/prices?product=${service}&country=${country}`, {
67
+ headers: {
68
+ 'Authorization': `Bearer ${this.apiKey}`,
69
+ 'Accept': 'application/json',
70
+ }
71
+ });
72
+
73
+ return response.data;
74
+ } catch (error: any) {
75
+ logger.error(`Error fetching prices for ${service} in ${country}: ${error.message}`);
76
+ throw new Error(`Failed to fetch prices: ${error.message}`);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Purchase a number for a specific service and country
82
+ */
83
+ async purchaseNumber(service: string, country: string, operator: string): Promise<any> {
84
+ try {
85
+ const response = await axios.get(
86
+ `${this.baseUrl}/user/buy/activation/${country}/${operator}/${service}`,
87
+ {
88
+ headers: {
89
+ 'Authorization': `Bearer ${this.apiKey}`,
90
+ 'Accept': 'application/json',
91
+ }
92
+ }
93
+ );
94
+
95
+ return response.data;
96
+ } catch (error: any) {
97
+ logger.error(`Error purchasing number for ${service} in ${country}: ${error.message}`);
98
+ throw new Error(`Failed to purchase number: ${error.message}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check for SMS received for a specific order
104
+ */
105
+ async checkSMS(orderId: string | number): Promise<any> {
106
+ try {
107
+ const response = await axios.get(
108
+ `${this.baseUrl}/user/check/${orderId}`,
109
+ {
110
+ headers: {
111
+ 'Authorization': `Bearer ${this.apiKey}`,
112
+ 'Accept': 'application/json',
113
+ }
114
+ }
115
+ );
116
+
117
+ return response.data;
118
+ } catch (error: any) {
119
+ logger.error(`Error checking SMS for order ${orderId}: ${error.message}`);
120
+ throw new Error(`Failed to check SMS: ${error.message}`);
121
+ }
122
+ }
123
+ }
src/bots/services/auth.ts ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ fetchDataFromTable,
3
+ insertDataIntoTable,
4
+ updateDataInTable
5
+ } from '../../db/supabaseHelper';
6
+ import { createLogger } from '../../utils/logger';
7
+ import { v4 as uuidv4 } from 'uuid';
8
+ import { supabase } from '../../db/supabase';
9
+
10
+ const logger = createLogger('AuthService');
11
+
12
+ interface UserBotTelegraf {
13
+ id?: number; // SERIAL in SQL becomes number in TypeScript
14
+ telegramId: number; // BIGINT in SQL becomes number in TypeScript
15
+ username?: string | null; // VARCHAR(255), optional
16
+ firstName?: string | null; // VARCHAR(255), matches SQL column name
17
+ lastName?: string | null; // VARCHAR(255), matches SQL column name
18
+ email: string; // VARCHAR(255) NOT NULL
19
+ passwordHash: string; // VARCHAR(255) NOT NULL, matches SQL column name
20
+ language?: string; // VARCHAR(10) DEFAULT 'en'
21
+ role?: string; // VARCHAR(10) DEFAULT 'user'
22
+ balance?: number; // DECIMAL(10, 2) DEFAULT 0
23
+ isBanned?: boolean; // BOOLEAN DEFAULT false
24
+ lastLogin?: Date | string | null; // TIMESTAMPTZ (can be Date, ISO string, or null)
25
+ botId?: string | null; // UUID (string in TypeScript)
26
+ createdAt?: Date | string; // TIMESTAMPTZ DEFAULT NOW()
27
+ updatedAt?: Date | string; // TIMESTAMPTZ DEFAULT NOW()
28
+ }
29
+
30
+ export class AuthService {
31
+ private static instance: AuthService;
32
+
33
+ private constructor() {
34
+ // Private constructor for singleton pattern
35
+ }
36
+
37
+ public static getInstance(): AuthService {
38
+ if (!AuthService.instance) {
39
+ AuthService.instance = new AuthService();
40
+ }
41
+ return AuthService.instance;
42
+ }
43
+
44
+ private generatePassword(): string {
45
+ return uuidv4().replace(/-/g, '').substring(0, 12);
46
+ }
47
+
48
+ public async hashPassword(password: string): Promise<string> {
49
+ const encoder = new TextEncoder();
50
+ const data = encoder.encode(password);
51
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
52
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
53
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
54
+ }
55
+
56
+ private generateEmail(telegramId: number): string {
57
+ return `user_${telegramId}@saerosms.com`;
58
+ }
59
+
60
+ public async createUser(telegramId: number, name: string, botId:string): Promise<{ user: UserBotTelegraf; password: string }> {
61
+ try {
62
+ const email = this.generateEmail(telegramId);
63
+ const password = this.generatePassword();
64
+ const passwordHash = await this.hashPassword(password);
65
+
66
+ const userData: Omit<UserBotTelegraf, 'id'> = {
67
+ telegramId: telegramId,
68
+ firstName: name,
69
+ email,
70
+ passwordHash: passwordHash,
71
+ botId:botId
72
+ };
73
+
74
+ const user = await insertDataIntoTable<UserBotTelegraf>('users_bot_telegram', userData);
75
+
76
+ logger.info(`User ${telegramId} created successfully`);
77
+ return { user, password };
78
+
79
+ } catch (error: any) {
80
+ logger.error(`Error creating user ${telegramId}: ${error.message}`);
81
+ throw new Error(`Failed to create user: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ private loggedInUsers: Map<number, boolean> = new Map();
86
+
87
+ public setUserLoggedIn(telegramId: number, isLoggedIn: boolean = true): void {
88
+ this.loggedInUsers.set(telegramId, isLoggedIn);
89
+ logger.info(`User ${telegramId} login state set to ${isLoggedIn}`);
90
+ }
91
+
92
+ public isUserLoggedIn(telegramId: number): boolean {
93
+ return this.loggedInUsers.get(telegramId) || false;
94
+ }
95
+
96
+ public async loginUser(telegramId: number): Promise<UserBotTelegraf | null> {
97
+ try {
98
+ const { data: users } = await fetchDataFromTable<UserBotTelegraf>('users_bot_telegram', 1000, 0, {
99
+ telegram_id: telegramId,
100
+ });
101
+
102
+ if (!users || users.length === 0) {
103
+ logger.info(`Login attempt for non-existent user ${telegramId}`);
104
+ return null;
105
+ }
106
+
107
+ const user = users[0];
108
+
109
+ // Update last login
110
+ await updateDataInTable<UserBotTelegraf>('users_bot_telegram', {
111
+ ...user,
112
+ lastLogin: new Date().toISOString()
113
+ });
114
+
115
+ // Set user as logged in
116
+ this.setUserLoggedIn(telegramId, true);
117
+
118
+ logger.info(`User ${telegramId} logged in successfully`);
119
+ return user;
120
+
121
+ } catch (error: any) {
122
+ logger.error(`Error logging in user ${telegramId}: ${error.message}`);
123
+ throw new Error(`Failed to login user: ${error.message}`);
124
+ }
125
+ }
126
+
127
+ public async getUserById(userId: string): Promise<UserBotTelegraf | null> {
128
+ try {
129
+ const { data: users } = await fetchDataFromTable<UserBotTelegraf>('users_bot_telegram', 1000, 0, {
130
+ id: userId
131
+ });
132
+
133
+ return users[0] || null;
134
+ } catch (error: any) {
135
+ logger.error(`Error fetching user ${userId}: ${error.message}`);
136
+ throw new Error(`Failed to get user: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get user by Telegram ID
142
+ */
143
+ async getUserByTelegramId(telegramId: number): Promise<any> {
144
+ try {
145
+ const { data: users, error } = await supabase
146
+ .from('users_bot_telegram')
147
+ .select('*')
148
+ .eq('telegram_id', telegramId)
149
+ .single();
150
+
151
+ if (error) throw error;
152
+ return users;
153
+ } catch (error: any) {
154
+ logger.error(`Error getting user by Telegram ID: ${error.message}`);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Update user balance
161
+ */
162
+ async updateUserBalance(telegramId: number, newBalance: number): Promise<boolean> {
163
+ try {
164
+ const { error } = await supabase
165
+ .from('users_bot_telegram')
166
+ .update({ balance: newBalance })
167
+ .eq('telegram_id', telegramId);
168
+
169
+ if (error) throw error;
170
+ return true;
171
+ } catch (error: any) {
172
+ logger.error(`Error updating user balance: ${error.message}`);
173
+ return false;
174
+ }
175
+ }
176
+ }
177
+
178
+ // Singleton instance
179
+ export const authService = AuthService.getInstance();
src/bots/types/botTypes.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Context } from 'telegraf';
2
+ import { Update } from 'telegraf/typings/core/types/typegram';
3
+
4
+ export type BotCommand = {
5
+ command: string;
6
+ description: string;
7
+ };
8
+
9
+ export interface BotContext extends Context<Update> {
10
+ botData?: any;
11
+ match?: RegExpMatchArray;
12
+ customData?: any;
13
+ webhookData?: {
14
+ paymentId: string;
15
+ status: 'COMPLETED' | 'FAILED';
16
+ };
17
+ }
src/bots/types/paymentTypes.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type PaymentMethod = 'PAYPAL' | 'CRYPTO' | 'ADMIN';
2
+
3
+ export interface PaymentInfo {
4
+ id: string;
5
+ userId: string;
6
+ amount: number;
7
+ method: PaymentMethod;
8
+ status: 'PENDING' | 'COMPLETED' | 'FAILED';
9
+ createdAt: Date;
10
+ updatedAt: Date;
11
+ }
src/bots/utils/country.ts ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface CountryInfo {
2
+ label: string;
3
+ flag: string;
4
+ }
5
+
6
+ interface CountryData {
7
+ [key: string]: CountryInfo;
8
+ }
9
+
10
+ export const countryData: CountryData = {
11
+ afghanistan: { label: "Afghanistan", flag: "🇦🇫" },
12
+ albania: { label: "Albania", flag: "🇦🇱" },
13
+ algeria: { label: "Algeria", flag: "🇩🇿" },
14
+ angola: { label: "Angola", flag: "🇦🇴" },
15
+ antiguaandbarbuda: { label: "Antigua and Barbuda", flag: "🇦🇬" },
16
+ argentina: { label: "Argentina", flag: "🇦🇷" },
17
+ armenia: { label: "Armenia", flag: "🇦🇲" },
18
+ aruba: { label: "Aruba", flag: "🇦🇼" },
19
+ australia: { label: "Australia", flag: "🇦🇺" },
20
+ austria: { label: "Austria", flag: "🇦🇹" },
21
+ azerbaijan: { label: "Azerbaijan", flag: "🇦🇿" },
22
+ bahamas: { label: "Bahamas", flag: "🇧🇸" },
23
+ bahrain: { label: "Bahrain", flag: "🇧🇭" },
24
+ bangladesh: { label: "Bangladesh", flag: "🇧🇩" },
25
+ barbados: { label: "Barbados", flag: "🇧🇧" },
26
+ belarus: { label: "Belarus", flag: "🇧🇾" },
27
+ belgium: { label: "Belgium", flag: "🇧🇪" },
28
+ belize: { label: "Belize", flag: "🇧🇿" },
29
+ benin: { label: "Benin", flag: "🇧🇯" },
30
+ bhutane: { label: "Bhutan", flag: "🇧🇹" },
31
+ bih: { label: "Bosnia and Herzegovina", flag: "🇧🇦" },
32
+ bolivia: { label: "Bolivia", flag: "🇧🇴" },
33
+ botswana: { label: "Botswana", flag: "🇧🇼" },
34
+ brazil: { label: "Brazil", flag: "🇧🇷" },
35
+ bulgaria: { label: "Bulgaria", flag: "🇧🇬" },
36
+ burkinafaso: { label: "Burkina Faso", flag: "🇧🇫" },
37
+ burundi: { label: "Burundi", flag: "🇧🇮" },
38
+ cambodia: { label: "Cambodia", flag: "🇰🇭" },
39
+ cameroon: { label: "Cameroon", flag: "🇨🇲" },
40
+ canada: { label: "Canada", flag: "🇨🇦" },
41
+ capeverde: { label: "Cape Verde", flag: "🇨🇻" },
42
+ chad: { label: "Chad", flag: "🇹🇩" },
43
+ chile: { label: "Chile", flag: "🇨🇱" },
44
+ colombia: { label: "Colombia", flag: "🇨🇴" },
45
+ comoros: { label: "Comoros", flag: "🇰🇲" },
46
+ congo: { label: "Congo", flag: "🇨🇬" },
47
+ costarica: { label: "Costa Rica", flag: "🇨🇷" },
48
+ croatia: { label: "Croatia", flag: "🇭🇷" },
49
+ cyprus: { label: "Cyprus", flag: "🇨🇾" },
50
+ czech: { label: "Czech Republic", flag: "🇨🇿" },
51
+ denmark: { label: "Denmark", flag: "🇩🇰" },
52
+ djibouti: { label: "Djibouti", flag: "🇩🇯" },
53
+ dominicana: { label: "Dominican Republic", flag: "🇩🇴" },
54
+ easttimor: { label: "East Timor", flag: "🇹🇱" },
55
+ ecuador: { label: "Ecuador", flag: "🇪🇨" },
56
+ egypt: { label: "Egypt", flag: "🇪🇬" },
57
+ england: { label: "England", flag: "🏴" },
58
+ equatorialguinea: { label: "Equatorial Guinea", flag: "🇬🇶" },
59
+ estonia: { label: "Estonia", flag: "🇪🇪" },
60
+ ethiopia: { label: "Ethiopia", flag: "🇪🇹" },
61
+ finland: { label: "Finland", flag: "🇫🇮" },
62
+ france: { label: "France", flag: "🇫🇷" },
63
+ frenchguiana: { label: "French Guiana", flag: "🇬🇫" },
64
+ gabon: { label: "Gabon", flag: "🇬🇦" },
65
+ gambia: { label: "Gambia", flag: "🇬🇲" },
66
+ georgia: { label: "Georgia", flag: "🇬🇪" },
67
+ germany: { label: "Germany", flag: "🇩🇪" },
68
+ ghana: { label: "Ghana", flag: "🇬🇭" },
69
+ gibraltar: { label: "Gibraltar", flag: "🇬🇮" },
70
+ greece: { label: "Greece", flag: "🇬🇷" },
71
+ guadeloupe: { label: "Guadeloupe", flag: "🇬🇵" },
72
+ guatemala: { label: "Guatemala", flag: "🇬🇹" },
73
+ guinea: { label: "Guinea", flag: "🇬🇳" },
74
+ guineabissau: { label: "Guinea-Bissau", flag: "🇬🇼" },
75
+ guyana: { label: "Guyana", flag: "🇬🇾" },
76
+ haiti: { label: "Haiti", flag: "🇭🇹" },
77
+ honduras: { label: "Honduras", flag: "🇭🇳" },
78
+ hongkong: { label: "Hong Kong", flag: "🇭🇰" },
79
+ hungary: { label: "Hungary", flag: "🇭🇺" },
80
+ india: { label: "India", flag: "🇮🇳" },
81
+ indonesia: { label: "Indonesia", flag: "🇮🇩" },
82
+ ireland: { label: "Ireland", flag: "🇮🇪" },
83
+ israel: { label: "Israel", flag: "🇮🇱" },
84
+ italy: { label: "Italy", flag: "🇮🇹" },
85
+ ivorycoast: { label: "Ivory Coast", flag: "🇨🇮" },
86
+ jamaica: { label: "Jamaica", flag: "🇯🇲" },
87
+ jordan: { label: "Jordan", flag: "🇯🇴" },
88
+ kazakhstan: { label: "Kazakhstan", flag: "🇰🇿" },
89
+ kenya: { label: "Kenya", flag: "🇰🇪" },
90
+ kuwait: { label: "Kuwait", flag: "🇰🇼" },
91
+ kyrgyzstan: { label: "Kyrgyzstan", flag: "🇰🇬" },
92
+ laos: { label: "Laos", flag: "🇱🇦" },
93
+ latvia: { label: "Latvia", flag: "🇱🇻" },
94
+ lesotho: { label: "Lesotho", flag: "🇱🇸" },
95
+ liberia: { label: "Liberia", flag: "🇱🇷" },
96
+ lithuania: { label: "Lithuania", flag: "🇱🇹" },
97
+ luxembourg: { label: "Luxembourg", flag: "🇱🇺" },
98
+ macau: { label: "Macau", flag: "🇲🇴" },
99
+ madagascar: { label: "Madagascar", flag: "🇲🇬" },
100
+ malawi: { label: "Malawi", flag: "🇲🇼" },
101
+ malaysia: { label: "Malaysia", flag: "🇲🇾" },
102
+ maldives: { label: "Maldives", flag: "🇲🇻" },
103
+ mauritania: { label: "Mauritania", flag: "🇲🇷" },
104
+ mauritius: { label: "Mauritius", flag: "🇲🇺" },
105
+ mexico: { label: "Mexico", flag: "🇲🇽" },
106
+ moldova: { label: "Moldova", flag: "🇲🇩" },
107
+ mongolia: { label: "Mongolia", flag: "🇲🇳" },
108
+ montenegro: { label: "Montenegro", flag: "🇲🇪" },
109
+ morocco: { label: "Morocco", flag: "🇲🇦" },
110
+ mozambique: { label: "Mozambique", flag: "🇲🇿" },
111
+ namibia: { label: "Namibia", flag: "🇳🇦" },
112
+ nepal: { label: "Nepal", flag: "🇳🇵" },
113
+ netherlands: { label: "Netherlands", flag: "🇳🇱" },
114
+ newcaledonia: { label: "New Caledonia", flag: "🇳🇨" },
115
+ newzealand: { label: "New Zealand", flag: "🇳🇿" },
116
+ nicaragua: { label: "Nicaragua", flag: "🇳🇮" },
117
+ nigeria: { label: "Nigeria", flag: "🇳🇬" },
118
+ northmacedonia: { label: "North Macedonia", flag: "🇲🇰" },
119
+ norway: { label: "Norway", flag: "🇳🇴" },
120
+ oman: { label: "Oman", flag: "🇴🇲" },
121
+ pakistan: { label: "Pakistan", flag: "🇵🇰" },
122
+ panama: { label: "Panama", flag: "🇵🇦" },
123
+ papuanewguinea: { label: "Papua New Guinea", flag: "🇵🇬" },
124
+ paraguay: { label: "Paraguay", flag: "🇵🇾" },
125
+ peru: { label: "Peru", flag: "🇵🇪" },
126
+ philippines: { label: "Philippines", flag: "🇵🇭" },
127
+ poland: { label: "Poland", flag: "🇵🇱" },
128
+ portugal: { label: "Portugal", flag: "🇵🇹" },
129
+ puertorico: { label: "Puerto Rico", flag: "🇵🇷" },
130
+ reunion: { label: "Réunion", flag: "🇷🇪" },
131
+ romania: { label: "Romania", flag: "🇷🇴" },
132
+ russia: { label: "Russia", flag: "🇷🇺" },
133
+ rwanda: { label: "Rwanda", flag: "🇷🇼" },
134
+ saintkittsandnevis: { label: "Saint Kitts and Nevis", flag: "🇰🇳" },
135
+ saintlucia: { label: "Saint Lucia", flag: "🇱🇨" },
136
+ saintvincentandgrenadines: { label: "Saint Vincent and the Grenadines", flag: "🇻🇨" },
137
+ salvador: { label: "El Salvador", flag: "🇸🇻" },
138
+ samoa: { label: "Samoa", flag: "🇼🇸" },
139
+ saudiarabia: { label: "Saudi Arabia", flag: "🇸🇦" },
140
+ senegal: { label: "Senegal", flag: "🇸🇳" },
141
+ serbia: { label: "Serbia", flag: "🇷🇸" },
142
+ seychelles: { label: "Seychelles", flag: "🇸🇨" },
143
+ sierraleone: { label: "Sierra Leone", flag: "🇸🇱" },
144
+ singapore: { label: "Singapore", flag: "🇸🇬" },
145
+ slovakia: { label: "Slovakia", flag: "🇸🇰" },
146
+ slovenia: { label: "Slovenia", flag: "🇸🇮" },
147
+ solomonislands: { label: "Solomon Islands", flag: "🇸🇧" },
148
+ southafrica: { label: "South Africa", flag: "🇿🇦" },
149
+ spain: { label: "Spain", flag: "🇪🇸" },
150
+ srilanka: { label: "Sri Lanka", flag: "🇱🇰" },
151
+ suriname: { label: "Suriname", flag: "🇸🇷" },
152
+ swaziland: { label: "Eswatini", flag: "🇸🇿" },
153
+ sweden: { label: "Sweden", flag: "🇸🇪" },
154
+ switzerland: { label: "Switzerland", flag: "🇨🇭" },
155
+ taiwan: { label: "Taiwan", flag: "🇹🇼" },
156
+ tajikistan: { label: "Tajikistan", flag: "🇹🇯" },
157
+ tanzania: { label: "Tanzania", flag: "🇹🇿" },
158
+ thailand: { label: "Thailand", flag: "🇹🇭" },
159
+ tit: { label: "East Timor", flag: "🇹🇱" },
160
+ togo: { label: "Togo", flag: "🇹🇬" },
161
+ tunisia: { label: "Tunisia", flag: "🇹🇳" },
162
+ turkmenistan: { label: "Turkmenistan", flag: "🇹🇲" },
163
+ uganda: { label: "Uganda", flag: "🇺🇬" },
164
+ ukraine: { label: "Ukraine", flag: "🇺🇦" },
165
+ uruguay: { label: "Uruguay", flag: "🇺🇾" },
166
+ usa: { label: "United States", flag: "🇺🇸" },
167
+ uzbekistan: { label: "Uzbekistan", flag: "🇺🇿" },
168
+ venezuela: { label: "Venezuela", flag: "🇻🇪" },
169
+ vietnam: { label: "Vietnam", flag: "🇻🇳" },
170
+ zambia: { label: "Zambia", flag: "🇿🇲" }
171
+ };
src/bots/utils/handlerUtils.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // utils/handlerUtils.ts
2
+
3
+ import { BotContext } from "../types/botTypes";
4
+ import { createLogger } from '../../utils/logger';
5
+
6
+ const logger = createLogger('HandlerUtils');
7
+
8
+ /**
9
+ * Creates a standardized message reply handler with error handling
10
+ * @param replyFunction Function that returns the message and options to send
11
+ * @returns A function that handles the reply with proper error handling
12
+ */
13
+ export const messageReplyHandler = (
14
+ replyFunction: (ctx: BotContext) => Promise<{message: string, options?: any}> | {message: string, options?: any}
15
+ ) => async (ctx: BotContext) => {
16
+ try {
17
+ // Execute the reply function to get message content and options
18
+ const result = await Promise.resolve(replyFunction(ctx));
19
+
20
+ // Get the message ID to reply to
21
+ const replyToMessageId = ctx.message?.message_id;
22
+
23
+ // Add reply_to_message_id to options if not already set
24
+ const options = {
25
+ ...result.options,
26
+ reply_to_message_id: replyToMessageId
27
+ };
28
+
29
+ // Send the reply with the message and options
30
+ return await ctx.reply(result.message, options);
31
+ } catch (error: any) {
32
+ // Log the error
33
+ logger.error(`Error in message reply handler: ${error.message}`);
34
+
35
+ // Send a generic error message to the user
36
+ return ctx.reply("⚠️ حدث خطأ أثناء معالجة طلبك. الرجاء المحاولة مرة أخرى.");
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Creates a standardized callback query handler with error handling
42
+ * @param replyFunction Function that returns the message and options to send after answering callback
43
+ * @returns A function that handles the callback with proper error handling
44
+ */
45
+ export const callbackReplyHandler = (
46
+ replyFunction: (ctx: BotContext) => Promise<{message: string, options?: any}> | {message: string, options?: any}
47
+ ) => async (ctx: BotContext) => {
48
+ try {
49
+ // Answer the callback query first
50
+ await ctx.answerCbQuery();
51
+
52
+ // Execute the reply function to get message content and options
53
+ const result = await Promise.resolve(replyFunction(ctx));
54
+
55
+ // Send the reply with the message and any provided options
56
+ return await ctx.reply(result.message, result.options);
57
+ } catch (error: any) {
58
+ // Log the error
59
+ logger.error(`Error in callback reply handler: ${error.message}`);
60
+
61
+ // Send a generic error message to the user
62
+ return ctx.reply("⚠️ حدث خطأ أثناء معالجة طلبك. الرجاء المحاولة مرة أخرى.");
63
+ }
64
+ };
src/bots/utils/keyboardUtils.ts ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Markup } from "telegraf";
2
+ import { countryData } from "./country";
3
+ import { formatPrice } from "./priceUtils";
4
+ import { BotContext } from "../types/botTypes";
5
+ import { messageManager } from "./messageManager";
6
+
7
+ export const getMainMenuKeyboard = () => {
8
+ return Markup.inlineKeyboard([
9
+ [Markup.button.callback(messageManager.getMessage('btn_login'), 'login')],
10
+ [Markup.button.callback(messageManager.getMessage('btn_terms'), 'terms')],
11
+ [Markup.button.callback(messageManager.getMessage('btn_new_members'), 'new_members')],
12
+ [Markup.button.callback(messageManager.getMessage('btn_stats'), 'stats')],
13
+ [Markup.button.callback(messageManager.getMessage('btn_change_language'), 'change_language')],
14
+ ]);
15
+ };
16
+
17
+ export const getLoggedInMenuKeyboard = () => {
18
+ return Markup.inlineKeyboard([
19
+ [
20
+ Markup.button.callback(messageManager.getMessage('btn_service_telegram'), 'service_telegram'),
21
+ Markup.button.callback(messageManager.getMessage('btn_service_whatsapp'), 'service_whatsapp')
22
+ ],
23
+ [
24
+ Markup.button.callback(messageManager.getMessage('btn_service_facebook'), 'service_facebook'),
25
+ Markup.button.callback(messageManager.getMessage('btn_service_instagram'), 'service_instagram')
26
+ ],
27
+ [
28
+ Markup.button.callback(messageManager.getMessage('btn_service_tiktok'), 'service_tiktok'),
29
+ Markup.button.callback(messageManager.getMessage('btn_service_twitter'), 'service_twitter')
30
+ ],
31
+ [
32
+ Markup.button.callback(messageManager.getMessage('btn_service_snapchat'), 'service_snapchat'),
33
+ Markup.button.callback(messageManager.getMessage('btn_service_google'), 'service_google')
34
+ ],
35
+ [
36
+ Markup.button.callback(messageManager.getMessage('btn_service_imo'), 'service_imo'),
37
+ ],
38
+ [
39
+ Markup.button.callback(messageManager.getMessage('btn_profile'), 'profile'),
40
+ Markup.button.callback(messageManager.getMessage('btn_change_language'), 'change_language')
41
+ ],
42
+ [
43
+ Markup.button.callback(messageManager.getMessage('btn_top_up'), 'top_up_balance'),
44
+ Markup.button.callback(messageManager.getMessage('btn_history'), 'history')
45
+ ],
46
+ [
47
+ Markup.button.callback(messageManager.getMessage('btn_back'), 'main_menu')
48
+ ],
49
+ ]);
50
+ };
51
+
52
+ export const getBackToMainMenuButton = () => {
53
+ return Markup.inlineKeyboard([
54
+ [Markup.button.callback(messageManager.getMessage('btn_back'), 'main_menu')]
55
+ ]);
56
+ };
57
+
58
+ export const getCountriesKeyboard = (service: string, page: number = 0) => {
59
+ const buttons = [];
60
+ const rowSize = 2; // 2 buttons per row
61
+ const countriesPerPage = 15;
62
+
63
+ const countries = Object.entries(countryData).map(([id, data]) => ({
64
+ id,
65
+ name: data.label,
66
+ flag: data.flag
67
+ }));
68
+
69
+ const totalPages = Math.ceil(countries.length / countriesPerPage);
70
+ const startIndex = page * countriesPerPage;
71
+ const endIndex = Math.min(startIndex + countriesPerPage, countries.length);
72
+ const pageCountries = countries.slice(startIndex, endIndex);
73
+
74
+ for (let i = 0; i < pageCountries.length; i += rowSize) {
75
+ const row = [];
76
+ for (let j = 0; j < rowSize && i + j < pageCountries.length; j++) {
77
+ const country = pageCountries[i + j];
78
+ row.push(
79
+ Markup.button.callback(
80
+ `${country.name} ${country.flag}`,
81
+ `country_${service}_${country.id}`
82
+ )
83
+ );
84
+ }
85
+ buttons.push(row);
86
+ }
87
+
88
+ const paginationRow = [];
89
+ if (page > 0) {
90
+ paginationRow.push(
91
+ Markup.button.callback(
92
+ messageManager.getMessage('btn_previous'),
93
+ `page_${service}_${page - 1}`
94
+ )
95
+ );
96
+ }
97
+
98
+ paginationRow.push(
99
+ Markup.button.callback(
100
+ messageManager.getMessage('btn_page_info')
101
+ .replace('{current}', (page + 1).toString())
102
+ .replace('{total}', totalPages.toString()),
103
+ 'noop'
104
+ )
105
+ );
106
+
107
+ if (page < totalPages - 1) {
108
+ paginationRow.push(
109
+ Markup.button.callback(
110
+ messageManager.getMessage('btn_next'),
111
+ `page_${service}_${page + 1}`
112
+ )
113
+ );
114
+ }
115
+
116
+ if (paginationRow.length > 0) {
117
+ buttons.push(paginationRow);
118
+ }
119
+
120
+ buttons.push([
121
+ Markup.button.callback(
122
+ messageManager.getMessage('btn_main_menu'),
123
+ 'main_menu'
124
+ )
125
+ ]);
126
+
127
+ return Markup.inlineKeyboard(buttons);
128
+ };
129
+
130
+ export const getServicePricesKeyboard = (
131
+ prices: any,
132
+ service: string,
133
+ country: string,
134
+ ctx: BotContext
135
+ ) => {
136
+ const buttons = [];
137
+
138
+ const hasValidPrices = prices && Object.values(prices).some(
139
+ (priceInfo: any) => priceInfo && priceInfo.count > 0
140
+ );
141
+
142
+ if (!hasValidPrices) {
143
+ buttons.push([
144
+ Markup.button.callback(
145
+ messageManager.getMessage('btn_no_prices'),
146
+ 'noop'
147
+ ),
148
+ ]);
149
+ } else {
150
+ Object.entries(prices).forEach(([operator, priceInfo]: [string, any]) => {
151
+ if (priceInfo.count > 0) {
152
+ buttons.push([
153
+ Markup.button.callback(
154
+ messageManager.getMessage('btn_buy_format')
155
+ .replace('{price}', priceInfo.cost.toFixed(2))
156
+ .replace('{count}', priceInfo.count.toString()),
157
+ `buy_${service}_${country}_${operator}`
158
+ ),
159
+ ]);
160
+ }
161
+ });
162
+ }
163
+
164
+ buttons.push([
165
+ Markup.button.callback(
166
+ messageManager.getMessage('btn_back_to_services'),
167
+ `service_${service}`
168
+ ),
169
+ ]);
170
+
171
+ buttons.push([
172
+ Markup.button.callback(
173
+ messageManager.getMessage('btn_main_menu'),
174
+ 'main_menu'
175
+ )
176
+ ]);
177
+
178
+ return Markup.inlineKeyboard(buttons);
179
+ };
180
+
181
+ export const getHistoryKeyboard = () => {
182
+ return Markup.inlineKeyboard([
183
+ [Markup.button.callback(
184
+ messageManager.getMessage('btn_numbers_history'),
185
+ 'numbers_history'
186
+ )],
187
+ [Markup.button.callback(
188
+ messageManager.getMessage('btn_purchases_history'),
189
+ 'purchases_history'
190
+ )],
191
+ [Markup.button.callback(
192
+ messageManager.getMessage('btn_back_to_main'),
193
+ 'main_menu'
194
+ )]
195
+ ]);
196
+ };
197
+
198
+ export const getLanguageSelectionKeyboard = (isLoggedIn: boolean = false) => {
199
+ const buttons = [
200
+ [
201
+ Markup.button.callback(
202
+ messageManager.getMessage('btn_lang_english'),
203
+ 'set_language_en'
204
+ ),
205
+ Markup.button.callback(
206
+ messageManager.getMessage('btn_lang_arabic'),
207
+ 'set_language_ar'
208
+ )
209
+ ]
210
+ ];
211
+
212
+ buttons.push([
213
+ Markup.button.callback(
214
+ messageManager.getMessage('btn_back'),
215
+ isLoggedIn ? 'logged_in_menu' : 'main_menu'
216
+ )
217
+ ]);
218
+
219
+ return Markup.inlineKeyboard(buttons);
220
+ };
221
+
222
+
223
+ // Add new profile keyboard
224
+ export const getProfileKeyboard = () => {
225
+ return Markup.inlineKeyboard([
226
+ [Markup.button.callback(
227
+ messageManager.getMessage('btn_change_email'),
228
+ 'change_email'
229
+ )],
230
+ [Markup.button.callback(
231
+ messageManager.getMessage('btn_change_password'),
232
+ 'change_password'
233
+ )],
234
+ [Markup.button.callback(
235
+ messageManager.getMessage('btn_account_info'),
236
+ 'account_info'
237
+ )],
238
+ [Markup.button.callback(
239
+ messageManager.getMessage('btn_gift_balance'),
240
+ 'gift_balance'
241
+ )],
242
+ [Markup.button.callback(
243
+ messageManager.getMessage('btn_back_to_logged_in'),
244
+ 'logged_in_menu'
245
+ )]
246
+ ]);
247
+ };
src/bots/utils/messageManager.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { supabase } from '../../db/supabase';
2
+ import { createLogger } from '../../utils/logger';
3
+
4
+ const logger = createLogger('MessageManager');
5
+
6
+ interface BotMessage {
7
+ id: string;
8
+ bot_id: string;
9
+ key: string;
10
+ ar_value: string;
11
+ en_value: string;
12
+ description?: string;
13
+ created_at: string;
14
+ updated_at: string;
15
+ }
16
+
17
+ class MessageManager {
18
+ private static instance: MessageManager;
19
+ private messages: Map<string, BotMessage> = new Map();
20
+ private language: 'ar' | 'en' = 'ar';
21
+
22
+ private constructor() {}
23
+
24
+ static getInstance(): MessageManager {
25
+ if (!MessageManager.instance) {
26
+ MessageManager.instance = new MessageManager();
27
+ }
28
+ return MessageManager.instance;
29
+ }
30
+
31
+
32
+
33
+ setLanguage(lang: 'ar' | 'en') {
34
+ this.language = lang;
35
+ }
36
+
37
+ async loadMessages() {
38
+
39
+
40
+ try {
41
+ const { data, error } = await supabase
42
+ .from('bot_messages')
43
+ .select('*')
44
+
45
+
46
+ if (error) throw error;
47
+
48
+ this.messages.clear();
49
+ data.forEach((message: BotMessage) => {
50
+ this.messages.set(message.key, message);
51
+ });
52
+
53
+ logger.info(`Messages loaded successfully for bot `);
54
+ } catch (error) {
55
+ logger.error('Error loading messages:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ getMessage(key: string): string {
61
+ const message = this.messages.get(key);
62
+ if (!message) {
63
+ logger.warn(`Message not found for key: ${key}`);
64
+ return `[Message not found: ${key}]`;
65
+ }
66
+ return this.language === 'ar' ? message.ar_value : message.en_value;
67
+ }
68
+
69
+ async updateMessage(key: string, arValue: string, enValue: string, description?: string) {
70
+
71
+
72
+ try {
73
+ const { data, error } = await supabase
74
+ .from('bot_messages')
75
+ .upsert({
76
+
77
+ key,
78
+ ar_value: arValue,
79
+ en_value: enValue,
80
+ description
81
+ })
82
+ .select()
83
+ .single();
84
+
85
+ if (error) throw error;
86
+
87
+ if (data) {
88
+ this.messages.set(key, data);
89
+ }
90
+
91
+ return data;
92
+ } catch (error) {
93
+ logger.error('Error updating message:', error);
94
+ throw error;
95
+ }
96
+ }
97
+ }
98
+
99
+ export const messageManager = MessageManager.getInstance();
src/bots/utils/messageUtils.ts ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { formatPrice } from "./priceUtils";
3
+ import { messageManager } from "./messageManager";
4
+
5
+ export const getWelcomeMessage = (name: string = "عزيزي المستخدم") => {
6
+ return messageManager.getMessage('welcome').replace('{name}', name);
7
+ };
8
+
9
+ export const getLoginMessage = () => {
10
+ return messageManager.getMessage('login');
11
+ };
12
+
13
+ export const getTermsMessage = () => {
14
+ return messageManager.getMessage('terms');
15
+ };
16
+
17
+ export const getNewMembersMessage = () => {
18
+ return messageManager.getMessage('new_members');
19
+ };
20
+
21
+ export const getStatsMessage = () => {
22
+ return messageManager.getMessage('stats')
23
+ .replace('{current_datetime}', new Date().toLocaleString('ar-SA'));
24
+ };
25
+
26
+ export const getHelpMessage = () => {
27
+ return messageManager.getMessage('help');
28
+ };
29
+
30
+ export const getAboutMessage = () => {
31
+ return messageManager.getMessage('about');
32
+ };
33
+
34
+ export const getContactMessage = () => {
35
+ return messageManager.getMessage('contact');
36
+ };
37
+
38
+ export const getAuthRequiredMessage = () => {
39
+ return messageManager.getMessage('auth_required');
40
+ };
41
+
42
+ // export const getCountriesListMessage = (service: string, page: number = 0) => {
43
+ // return messageManager.getMessage('countries_list')
44
+ // .replace('{service}', service)
45
+ // .replace('{page > 0 ? `📑 الصفحة: {page + 1}\n` : \'\'}',
46
+ // page > 0 ? `📑 الصفحة: ${page + 1}\n` : '');
47
+ // };
48
+
49
+ export const getCountriesListMessage = (service: string, page: number = 0) => {
50
+ return `
51
+ 🌍 *الدول المتاحة لخدمة ${service}*
52
+
53
+ ${page > 0 ? `📑 الصفحة: ${page + 1}\n` : ''}✨ اختر الدولة التي تريدها من القائمة التالية:
54
+ `;
55
+ };
56
+ export const getServiceErrorMessage = (service: string) => {
57
+ return messageManager.getMessage('service_error')
58
+ .replace('{service}', service);
59
+ };
60
+
61
+ export const getLoadingPricesMessage = (service: string, countryId: string) => {
62
+ return messageManager.getMessage('loading_prices')
63
+ .replace('{service}', service)
64
+ .replace('{countryId}', countryId);
65
+ };
66
+
67
+ export const getNoPricesMessage = (service: string, countryId: string) => {
68
+ return messageManager.getMessage('no_prices')
69
+ .replace('{service}', service)
70
+ .replace('{countryId}', countryId);
71
+ };
72
+
73
+ export const getPricesListMessage = (
74
+ service: string,
75
+ countryId: string,
76
+ prices: any,
77
+ ctx: BotContext
78
+ ) => {
79
+ const countryName = countryId; // Consider using a real mapping function like getCountryName(countryId)
80
+ let message = messageManager.getMessage('prices_list')
81
+ .replace('{service}', service)
82
+ .replace('{countryName}', countryName);
83
+
84
+ // If you want to include prices in the future:
85
+ // Object.entries(prices).forEach(([operator, priceInfo]: [string, any]) => {
86
+ // const formattedPrice = formatPrice(ctx, priceInfo.cost);
87
+ // const availability = priceInfo.count > 0 ? ` (${priceInfo.count} متاح)` : ' (غير متوفر)';
88
+ // message += `${operator}: ${formattedPrice}${availability}\n`;
89
+ // });
90
+
91
+ return message;
92
+ };
93
+
94
+ export const getPricesErrorMessage = (service: string, countryId: string) => {
95
+ return messageManager.getMessage('prices_error')
96
+ .replace('{service}', service)
97
+ .replace('{countryId}', countryId);
98
+ };
99
+
100
+ export const getProcessingPurchaseMessage = (service: string, countryId: string) => {
101
+ return messageManager.getMessage('processing_purchase')
102
+ .replace('{service}', service)
103
+ .replace('{countryId}', countryId);
104
+ };
105
+
106
+ export const getServiceUnavailableMessage = (
107
+ service: string,
108
+ operator: string,
109
+ countryId: string
110
+ ) => {
111
+ return messageManager.getMessage('service_unavailable')
112
+ .replace('{service}', service)
113
+ .replace('{operator}', operator)
114
+ .replace('{countryId}', countryId);
115
+ };
116
+
117
+ export const getInsufficientBalanceMessage = (cost: number, balance: number) => {
118
+ return messageManager.getMessage('insufficient_balance')
119
+ .replace('{cost}', cost.toString())
120
+ .replace('{balance}', balance.toString());
121
+ };
122
+
123
+ export const getPurchaseSuccessMessage = (
124
+ purchaseResult: any,
125
+ service: string,
126
+ countryId: string,
127
+ operator: string,
128
+ cost: number
129
+ ) => {
130
+ return messageManager.getMessage('purchase_success')
131
+ .replace('{phone}', purchaseResult.phone)
132
+ .replace('{countryId}', countryId)
133
+ .replace('{operator}', operator)
134
+ .replace('{cost}', cost.toString())
135
+ .replace('{expires}', purchaseResult.expires.toString());
136
+ };
137
+
138
+ export const getPurchaseFailedMessage = () => {
139
+ return messageManager.getMessage('purchase_failed');
140
+ };
141
+
142
+ export const getPurchaseErrorMessage = (errorMessage: string) => {
143
+ return messageManager.getMessage('purchase_error')
144
+ .replace('{error_message}', errorMessage || 'سبب غير معروف');
145
+ };
146
+
147
+ export const getPaginationErrorMessage = () => {
148
+ return messageManager.getMessage('pagination_error');
149
+ };
150
+
151
+ export const getVerificationCodeMessage = (phone: string, code: string) => {
152
+ return messageManager.getMessage('verification_code')
153
+ .replace('{phone}', phone)
154
+ .replace('{code}', code)
155
+ .replace('{current_time}', new Date().toLocaleTimeString('ar-SA'));
156
+ };
157
+
158
+ export const getVerificationTimeoutMessage = () => {
159
+ return messageManager.getMessage('verification_timeout');
160
+ };
src/bots/utils/priceUtils.ts ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { createLogger } from "../../utils/logger";
3
+
4
+ const logger = createLogger('PriceUtils');
5
+
6
+ // Currency exchange rates (you might want to fetch these dynamically)
7
+ const exchangeRates: Record<string, number> = {
8
+ 'USD_RUB': 79.6229,
9
+ 'RUB_USD': 1 / 79.6229, // Add this
10
+ 'EUR_RUB': 86.5,
11
+ 'RUB_EUR': 1 / 86.5, // Add this
12
+ };
13
+
14
+ /**
15
+ * Convert price from one currency to another
16
+ *
17
+ * @param amount The amount to convert
18
+ * @param fromCurrency Source currency code
19
+ * @param toCurrency Target currency code
20
+ * @returns Converted amount
21
+ */
22
+ export const convertCurrency = (
23
+ amount: number,
24
+ fromCurrency: string = 'USD',
25
+ toCurrency: string = 'RUB'
26
+ ): number => {
27
+ try {
28
+ if (fromCurrency === toCurrency) {
29
+ return amount;
30
+ }
31
+
32
+ const rateKey = `${fromCurrency}_${toCurrency}`;
33
+ const rate = exchangeRates[rateKey];
34
+
35
+ if (!rate) {
36
+ logger.warn(`Exchange rate not found for ${rateKey}, returning original amount`);
37
+ return amount;
38
+ }
39
+
40
+ return amount * rate;
41
+ } catch (error: any) {
42
+ logger.error(`Error converting currency: ${error.message}`);
43
+ return amount;
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Calculate the final price with profit based on bot settings
49
+ *
50
+ * @param ctx The bot context containing botData
51
+ * @param originalPrice The original price to apply profit to
52
+ * @param originalCurrency The currency of the original price
53
+ * @returns The calculated price with profit applied
54
+ */
55
+ export const calculatePriceWithProfit = (
56
+ ctx: BotContext,
57
+ originalPrice: number,
58
+ originalCurrency: string = 'USD'
59
+ ): number => {
60
+ try {
61
+
62
+ // Check if botData exists
63
+ if (!ctx.botData) {
64
+ logger.warn('Bot data not available, returning original price');
65
+ return originalPrice;
66
+ }
67
+
68
+ const { profit_type, profit_value_percentage, profit_value_fix, currency } = ctx.botData;
69
+
70
+ // Convert to bot's currency if needed
71
+ const priceInBotCurrency = currency !== originalCurrency
72
+ ? convertCurrency(originalPrice, originalCurrency, currency)
73
+ : originalPrice;
74
+
75
+ // Calculate price based on profit type
76
+ let finalPrice = priceInBotCurrency;
77
+
78
+ if (profit_type === 'percentage' && profit_value_percentage) {
79
+ // Calculate percentage profit
80
+ const profitAmount = priceInBotCurrency * (profit_value_percentage / 100);
81
+ finalPrice = priceInBotCurrency + profitAmount;
82
+ } else if (profit_type === 'fix' && profit_value_fix) {
83
+ // Add fixed profit amount
84
+ finalPrice = priceInBotCurrency + profit_value_fix;
85
+ } else {
86
+ // Default case - return converted price if profit settings are invalid
87
+ logger.warn('Invalid profit configuration, returning converted price');
88
+ }
89
+
90
+ return finalPrice;
91
+ } catch (error: any) {
92
+ logger.error(`Error calculating price with profit: ${error.message}`);
93
+ return originalPrice; // Return original price in case of error
94
+ }
95
+ };
96
+
97
+ /**
98
+ * Format price with currency symbol based on bot settings
99
+ *
100
+ * @param ctx The bot context containing botData
101
+ * @param price The price to format
102
+ * @returns Formatted price string with currency
103
+ */
104
+ export const formatPrice = (ctx: BotContext, price: number): string => {
105
+ try {
106
+ // if (typeof price !== 'number' || isNaN(price)) {
107
+ // logger.error(`Invalid price type: ${typeof price} - ${JSON.stringify(price)}`);
108
+ // return 'N/A'; // or some fallback text
109
+ // }
110
+
111
+ const currency = ctx.botData?.currency || 'USD';
112
+
113
+ switch (currency) {
114
+ case 'RUB':
115
+ return `${price.toFixed(2)} ₽`;
116
+ case 'USD':
117
+ return `$${price.toFixed(2)}`;
118
+ case 'EUR':
119
+ return `€${price.toFixed(2)}`;
120
+ default:
121
+ return `${price.toFixed(2)} ${currency}`;
122
+ }
123
+ } catch (error: any) {
124
+ logger.error(`Error formatting price: ${error.message}`);
125
+ return 'N/A';
126
+ }
127
+ };
128
+
src/bots/utils/settings.ts ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { supabase } from "../../db/supabase";
2
+ import { createLogger } from "../../utils/logger";
3
+
4
+ const logger = createLogger('BotSettings');
5
+
6
+ /**
7
+ * Get bot settings from database
8
+ * @param botId Bot UUID
9
+ * @returns Bot settings object or null if not found
10
+ */
11
+ export const getBotSettings = async (botId: string) => {
12
+ try {
13
+ const { data, error } = await supabase
14
+ .from('bots')
15
+ .select('*')
16
+ .eq('id', botId)
17
+ .single();
18
+
19
+ if (error) {
20
+ logger.error(`Error fetching bot settings: ${error.message}`);
21
+ return null;
22
+ }
23
+
24
+ return {
25
+ ...data.settings,
26
+ currency: data.currency,
27
+ profit: {
28
+ type: data.profit_type,
29
+ percentage: data.profit_value_percentage,
30
+ fix: data.profit_value_fix
31
+ },
32
+ version: data.version
33
+ };
34
+ } catch (error: any) {
35
+ logger.error(`Unexpected error getting bot settings: ${error.message}`);
36
+ return null;
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Save bot state to database
42
+ * @param botId Bot UUID
43
+ * @param state State object to save
44
+ * @returns Success status
45
+ */
46
+ export const saveBotState = async (botId: string, state: Record<string, any>) => {
47
+ try {
48
+ const { error } = await supabase
49
+ .from('bots')
50
+ .update({
51
+ state: state,
52
+ last_activity: new Date().toISOString()
53
+ })
54
+ .eq('id', botId);
55
+
56
+ if (error) {
57
+ logger.error(`Error saving bot state: ${error.message}`);
58
+ return false;
59
+ }
60
+
61
+ return true;
62
+ } catch (error: any) {
63
+ logger.error(`Unexpected error saving bot state: ${error.message}`);
64
+ return false;
65
+ }
66
+ };
67
+
68
+ /**
69
+ * Calculate profit based on bot settings
70
+ * @param botId Bot UUID
71
+ * @param amount Base amount
72
+ * @returns Amount with profit applied
73
+ */
74
+ export const calculateProfit = async (botId: string, amount: number) => {
75
+ try {
76
+ const { data, error } = await supabase
77
+ .from('bots')
78
+ .select('profit_type, profit_value_percentage, profit_value_fix')
79
+ .eq('id', botId)
80
+ .single();
81
+
82
+ if (error || !data) {
83
+ logger.error(`Error fetching profit settings: ${error?.message}`);
84
+ return amount;
85
+ }
86
+
87
+ if (data.profit_type === 'percentage') {
88
+ return amount * (1 + data.profit_value_percentage / 100);
89
+ } else {
90
+ return amount + data.profit_value_fix;
91
+ }
92
+ } catch (error: any) {
93
+ logger.error(`Unexpected error calculating profit: ${error.message}`);
94
+ return amount;
95
+ }
96
+ };
src/config.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as dotenv from 'dotenv';
2
+ import { fetchDataFromTable } from './db/supabaseHelper';
3
+
4
+ dotenv.config();
5
+
6
+ export const config = {
7
+ BOT_TOKEN: process.env.BOT_TOKEN || '',
8
+ API_ID: parseInt(process.env.API_ID || ''),
9
+ API_HASH: process.env.API_HASH || '',
10
+ PHONE_NUMBER: process.env.PHONE_NUMBER || '',
11
+ STRING_SESSION: process.env.STRING_SESSION || '',
12
+ };
13
+
14
+ export const PORT = process.env.PORT || 7860;
15
+
16
+ // Default values (will be overridden by DB values when loaded)
17
+ export let COMMANDS: Record<string, string> = {
18
+ GO: ".",
19
+ };
20
+
21
+ export let MESSAGES: Record<string, string> = {
22
+ FORWARD_SUCCESS: "أتفضل",
23
+ };
24
+
25
+ // Function to load commands and messages from the database
26
+ export const loadCommandsAndMessages = async () => {
27
+ try {
28
+ const commands = await fetchDataFromTable('commands', 1000,0);
29
+ const messages = await fetchDataFromTable('messages', 1000,0);
30
+
31
+ // Reset COMMANDS object
32
+ COMMANDS = {};
33
+
34
+ // Populate COMMANDS
35
+ commands.forEach((cmd: any) => {
36
+ COMMANDS[cmd.key] = cmd.key;
37
+ });
38
+
39
+ // Reset MESSAGES object
40
+ MESSAGES = {};
41
+
42
+ // Populate MESSAGES
43
+ messages.forEach((msg: any) => {
44
+ MESSAGES[msg.key] = msg.value;
45
+ });
46
+
47
+ console.log('Commands and messages loaded from database');
48
+ } catch (error) {
49
+ console.error('Error loading commands and messages:', error);
50
+ }
51
+ };
52
+
53
+ // Load commands and messages initially
54
+ // loadCommandsAndMessages();