Mohammed Foud commited on
Commit
959b027
·
1 Parent(s): ab38ff2
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. .gitattributes +1 -0
  8. .gitignore +39 -0
  9. .npmrc +1 -0
  10. .vscode/extensions.json +3 -0
  11. .vscode/settings.json +21 -0
  12. Dockerfile +29 -0
  13. a.py +13 -0
  14. d.sh +7 -0
  15. db.sql +185 -0
  16. login_failed.png +3 -0
  17. package.json +55 -0
  18. pnpm-lock.yaml +0 -0
  19. run.sh +4 -0
  20. src/api/paymentWebhooks.ts +47 -0
  21. src/bots/botManager.ts +234 -0
  22. src/bots/db.sql +153 -0
  23. src/bots/handlers/balanceHandlers.ts +252 -0
  24. src/bots/handlers/commandHandlers.ts +150 -0
  25. src/bots/handlers/giftHandlers.ts +175 -0
  26. src/bots/handlers/historyHandlers.ts +155 -0
  27. src/bots/handlers/index.ts +45 -0
  28. src/bots/handlers/languageHandlers.ts +74 -0
  29. src/bots/handlers/mainMenuHandlers.ts +139 -0
  30. src/bots/handlers/paymentWebhookHandlers.ts +47 -0
  31. src/bots/handlers/profileHandlers.ts +320 -0
  32. src/bots/handlers/purchaseHandlers.ts +241 -0
  33. src/bots/handlers/serviceHandlers.ts +878 -0
  34. src/bots/index.ts +112 -0
  35. src/bots/middleware/groupCheckMiddleware.ts +52 -0
  36. src/bots/services/AdminService.ts +24 -0
  37. src/bots/services/BalanceUpdateService.ts +266 -0
  38. src/bots/services/CryptoService.ts +44 -0
  39. src/bots/services/PayPalService.ts +121 -0
  40. src/bots/services/PaymentVerificationService.ts +83 -0
  41. src/bots/services/PurchaseTrackingService.ts +174 -0
  42. src/bots/services/VirtualNumberService.ts +176 -0
  43. src/bots/services/auth.ts +226 -0
  44. src/bots/types/botTypes.ts +17 -0
  45. src/bots/types/paymentTypes.ts +11 -0
  46. src/bots/utils/5sim_products.json +0 -0
  47. src/bots/utils/country.ts +172 -0
  48. src/bots/utils/handlerUtils.ts +90 -0
  49. src/bots/utils/keyboardUtils.ts +355 -0
  50. src/bots/utils/messageManager.ts +155 -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
+ }
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
.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 ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Create writable logs directory
23
+ RUN mkdir -p logs && chmod 777 logs
24
+
25
+ # Expose the port your app runs on
26
+ EXPOSE 7860
27
+
28
+ # Start the application
29
+ 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,7 @@
 
 
 
 
 
 
 
 
1
+ git add .
2
+ git commit -m "all"
3
+ git push
4
+
5
+
6
+
7
+ # curl -X POST https://mfoud444-bot-me.hf.space/bots/telegraf/start-specific -H "Content-Type: application/json" -d '{"botId": "049a92c4-7654-43f6-8e6f-7ff5cce78995"}'
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);
login_failed.png ADDED

Git LFS Details

  • SHA256: c9acc9659ed8b1786cff2f70a9053ae432d4cd76aab077efabba669dbac2cfa0
  • Pointer size: 131 Bytes
  • Size of remote file: 536 kB
package.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "playwright": "^1.43.0"
41
+ },
42
+ "devDependencies": {
43
+ "@antfu/eslint-config": "^0.35.3",
44
+ "@types/cors": "^2.8.17",
45
+ "@types/express": "^5.0.0",
46
+ "@types/node": "^22.13.0",
47
+ "eslint": "^8.35.0",
48
+ "rimraf": "^4.3.0",
49
+ "ts-node": "^10.9.2",
50
+ "tsup": "^6.6.3",
51
+ "typescript": "^5.7.3",
52
+ "unicodedata": "^0.1.1",
53
+ "unorm": "^1.6.0"
54
+ }
55
+ }
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,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { getBotIdFromToken, saveBotTokenMapping, isValidBotToken } from "../utils/botUtils";
11
+ import { groupCheckMiddleware } from "./middleware/groupCheckMiddleware";
12
+ // import { balanceUpdateService } from "./services/BalanceUpdateService";
13
+
14
+ const logger = createLogger('BotManager');
15
+
16
+ export const BotCommands: { command: string; description: string }[] = [
17
+ { command: 'start', description: 'بدء البوت' },
18
+ { command: 'help', description: 'يعرض قائمة المساعدة' },
19
+ { command: 'about', description: 'معلومات عن البوت' },
20
+ { command: 'contact', description: 'تواصل معنا' },
21
+ { command: 'balance', description: 'رصيدي' },
22
+ { command: 'change_language', description: 'تغيير اللغة / Change language' },
23
+ ];
24
+
25
+ // Add this interface to match your database schema
26
+ export interface BotData {
27
+ id: string;
28
+ name: string;
29
+ user_id: string;
30
+ bot_token: string;
31
+ is_active: boolean;
32
+ currency: string;
33
+ profit_type: 'fix' | 'percentage';
34
+ profit_value_percentage: number;
35
+ profit_value_fix: number;
36
+ last_activity: string | null;
37
+ version: string;
38
+
39
+ // API Keys and External Service Configuration
40
+ fivesim_api_key: string;
41
+ paypal_client_id: string;
42
+ paypal_client_secret: string;
43
+ crypto_wallet_address: string;
44
+ admin_contact: string;
45
+
46
+ // Group Join Settings
47
+ join_group_required: boolean;
48
+ group_channel_username: string;
49
+
50
+ settings: Record<string, any>;
51
+ state: Record<string, any>;
52
+ suffix_email: string;
53
+ created_at: string;
54
+ updated_at: string;
55
+ }
56
+
57
+ // Update the initializeBot function to use our utilities
58
+ export const initializeBot = async (botToken: string, botData?: BotData) => {
59
+ try {
60
+ // Validate bot token
61
+ if (!isValidBotToken(botToken)) {
62
+ throw new Error('Invalid bot token format');
63
+ }
64
+
65
+ const bot = new Telegraf<BotContext>(botToken);
66
+
67
+ // Extract and save bot ID mapping
68
+ const botId = getBotIdFromToken(botToken);
69
+ saveBotTokenMapping(botToken, botId);
70
+
71
+ // Set bot ID and load messages
72
+ await messageManager.loadMessages();
73
+
74
+ // Set language based on bot settings or default to Arabic
75
+ messageManager.setLanguage(botData?.settings?.language || 'en');
76
+
77
+ // Add bot data to context for use in handlers
78
+ bot.context.botData = botData || null;
79
+
80
+ // Setup middleware to track activity
81
+ bot.use(async (ctx, next) => {
82
+ if (botData?.id) {
83
+ // Update last_activity in database
84
+ await supabase
85
+ .from('bots')
86
+ .update({
87
+ last_activity: new Date().toISOString()
88
+ })
89
+ .eq('id', botData.id);
90
+ }
91
+ return next();
92
+ });
93
+
94
+ // Add group check middleware
95
+ bot.use(groupCheckMiddleware);
96
+
97
+ // Setup command handlers
98
+ setupCommandHandlers(bot);
99
+
100
+ // Setup callback handlers
101
+ setupCallbackHandlers(bot);
102
+
103
+ // Initialize balance update service and ensure it's properly set up
104
+ try {
105
+
106
+ // balanceUpdateService.setBot(bot);
107
+
108
+ logger.info('Balance update service initialized successfully');
109
+ } catch (error: any) {
110
+ logger.error(`Failed to initialize balance update service: ${error.message}`);
111
+ // Continue initialization even if balance service fails
112
+ }
113
+
114
+ // Set bot commands
115
+ try {
116
+ await bot.telegram.setMyCommands(BotCommands, {
117
+ scope: { type: 'default' }
118
+ });
119
+ logger.info('Bot commands set successfully');
120
+ } catch (error: any) {
121
+ logger.error(`Failed to set bot commands: ${error.message}`);
122
+ // Continue initialization even if setting commands fails
123
+ }
124
+
125
+ return {
126
+ success: true,
127
+ message: botData?.name ? `Bot "${botData.name}" initialized successfully` : "Bot initialized successfully",
128
+ bot
129
+ };
130
+ } catch (error: any) {
131
+ logger.error(`Failed to initialize bot: ${error.message}`);
132
+ return { success: false, message: `Failed to initialize bot: ${error.message}` };
133
+ }
134
+ };
135
+
136
+ // Utility function to update bot status in database
137
+ const updateBotStatus = async (botId: string, isActive: boolean, error?: string) => {
138
+ try {
139
+ await supabase
140
+ .from('bots')
141
+ .update({
142
+ is_active: isActive,
143
+ last_activity: new Date().toISOString(),
144
+ state: {
145
+ status: isActive ? 'running' : 'error',
146
+ startedAt: isActive ? new Date().toISOString() : undefined,
147
+ error: error
148
+ }
149
+ })
150
+ .eq('id', botId);
151
+ } catch (error: any) {
152
+ logger.error(`Error updating bot status: ${error.message}`);
153
+ }
154
+ };
155
+
156
+ export const stopBot = async (botId: string) => {
157
+ try {
158
+ // Get bot token from database
159
+ const { data: botData, error } = await supabase
160
+ .from('bots')
161
+ .select('bot_token')
162
+ .eq('id', botId)
163
+ .single();
164
+
165
+ if (error || !botData) {
166
+ await updateBotStatus(botId, false, "Bot not found in database");
167
+ return { success: false, message: "Bot not found in database" };
168
+ }
169
+
170
+ const botToken = botData.bot_token;
171
+ const bot = telegrafBots.get(botToken);
172
+
173
+ if (bot) {
174
+ await bot.stop();
175
+ telegrafBots.delete(botToken);
176
+
177
+ // Update database
178
+ await updateBotStatus(botId, false);
179
+ return { success: true, message: "Bot stopped successfully" };
180
+ }
181
+
182
+ // Bot instance not found
183
+ await updateBotStatus(botId, false, "Bot instance not found");
184
+
185
+ return { success: false, message: "Bot instance not found" };
186
+ } catch (error: any) {
187
+ logger.error(`Error stopping bot: ${error.message}`);
188
+ await updateBotStatus(botId, false, error.message);
189
+ return { success: false, message: `Failed to stop bot: ${error.message}` };
190
+ }
191
+ };
192
+
193
+ // New function to update bot settings
194
+ export const updateBotSettings = async (botId: string, settings: Record<string, any>) => {
195
+ try {
196
+ // Get bot data from database
197
+ const { data: botData, error } = await supabase
198
+ .from('bots')
199
+ .select('bot_token, settings')
200
+ .eq('id', botId)
201
+ .single();
202
+
203
+ if (error || !botData) {
204
+ return { success: false, message: "Bot not found in database" };
205
+ }
206
+
207
+ // Merge existing settings with new ones
208
+ const updatedSettings = { ...(botData.settings || {}), ...settings };
209
+
210
+ // Update database
211
+ await supabase
212
+ .from('bots')
213
+ .update({
214
+ settings: updatedSettings,
215
+ updated_at: new Date().toISOString()
216
+ })
217
+ .eq('id', botId);
218
+
219
+ // Update running bot if it exists
220
+ const bot = telegrafBots.get(botData.bot_token);
221
+ if (bot && bot.context) {
222
+ bot.context.botData = {
223
+ ...(bot.context.botData || {}),
224
+ settings: updatedSettings
225
+ };
226
+ }
227
+
228
+ return { success: true, message: "Bot settings updated successfully" };
229
+ } catch (error: any) {
230
+ logger.error(`Error updating bot settings: ${error.message}`);
231
+ return { success: false, message: `Failed to update bot settings: ${error.message}` };
232
+ }
233
+ };
234
+
src/bots/db.sql ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 NOT NULL, -- Telegram user ID (not unique)
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
+ UNIQUE(telegram_id, bot_id) -- Composite unique constraint
19
+ );
20
+
21
+ COMMENT ON TABLE public.users_bot_telegram IS 'Stores Telegram user accounts and their associated data.';
22
+
23
+ -- Enable Row Level Security
24
+ ALTER TABLE public.users_bot_telegram ENABLE ROW LEVEL SECURITY;
25
+
26
+ -- Policies
27
+ CREATE POLICY "Users can manage their own data." ON public.users_bot_telegram
28
+ FOR ALL USING (true);
29
+
30
+ -- Trigger for updating timestamps
31
+ CREATE TRIGGER update_users_bot_telegram_timestamp BEFORE UPDATE ON public.users_bot_telegram
32
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
33
+
34
+ -- Transactions table
35
+ CREATE TABLE IF NOT EXISTS public.transactions (
36
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
37
+ user_id INTEGER NOT NULL REFERENCES public.users_bot_telegram(id) ON DELETE CASCADE, -- Reference to user
38
+ agent_id INTEGER REFERENCES public.users_bot_telegram(id) ON DELETE SET NULL, -- Reference to agent who processed transaction
39
+ type VARCHAR(20) NOT NULL, -- Type of transaction (deposit/withdrawal/etc.)
40
+ amount DECIMAL(10, 2) NOT NULL, -- Transaction amount
41
+ reference_id VARCHAR(255), -- External reference ID
42
+ description TEXT, -- Transaction description
43
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- Timestamp when transaction was created
44
+ );
45
+
46
+ COMMENT ON TABLE public.transactions IS 'Stores financial transactions for users.';
47
+
48
+ -- Recharge cards table
49
+ CREATE TABLE IF NOT EXISTS public.recharge_cards (
50
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
51
+ code VARCHAR(50) UNIQUE NOT NULL, -- Unique card code
52
+ amount DECIMAL(10, 2) NOT NULL, -- Card value amount
53
+ is_used BOOLEAN NOT NULL DEFAULT false, -- Whether card has been used
54
+ is_reusable BOOLEAN NOT NULL DEFAULT false, -- Whether card can be reused
55
+ created_by INTEGER NOT NULL REFERENCES public.users_bot_telegram(id) ON DELETE CASCADE, -- Who created the card
56
+ used_by INTEGER REFERENCES public.users_bot_telegram(id) ON DELETE SET NULL, -- Who used the card
57
+ used_at TIMESTAMPTZ, -- When card was used
58
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When card was created
59
+ expires_at TIMESTAMPTZ -- When card expires
60
+ );
61
+
62
+ COMMENT ON TABLE public.recharge_cards IS 'Stores recharge cards for user balance top-ups.';
63
+
64
+ -- Phone numbers table
65
+ CREATE TABLE IF NOT EXISTS public.phone_numbers (
66
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
67
+ user_id INTEGER NOT NULL REFERENCES public.users_bot_telegram(id) ON DELETE CASCADE, -- Owner of the number
68
+ country_code VARCHAR(10) NOT NULL, -- Country code for number
69
+ service VARCHAR(50) NOT NULL, -- Service the number is for
70
+ number VARCHAR(50) NOT NULL, -- Phone number
71
+ price DECIMAL(10, 2) NOT NULL, -- Cost of the number
72
+ status VARCHAR(20) NOT NULL DEFAULT 'pending', -- Current status of number
73
+ fivesim_id VARCHAR(50) NOT NULL, -- Reference to 5sim.net ID
74
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When number was acquired
75
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When number was last updated
76
+ expires_at TIMESTAMPTZ -- When number expires
77
+ );
78
+
79
+ COMMENT ON TABLE public.phone_numbers IS 'Stores phone numbers acquired for verification services.';
80
+
81
+ -- Trigger for updating timestamps
82
+ CREATE TRIGGER update_phone_numbers_timestamp BEFORE UPDATE ON public.phone_numbers
83
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
84
+
85
+ -- SMS messages table
86
+ CREATE TABLE IF NOT EXISTS public.sms_messages (
87
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
88
+ phone_number_id INTEGER NOT NULL REFERENCES public.phone_numbers(id) ON DELETE CASCADE, -- Associated phone number
89
+ code VARCHAR(50) NOT NULL, -- Verification code from SMS
90
+ text TEXT NOT NULL, -- Full SMS text
91
+ received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When SMS was received
92
+ is_delivered BOOLEAN NOT NULL DEFAULT false, -- Whether SMS was delivered to user
93
+ delivered_at TIMESTAMPTZ -- When SMS was delivered to user
94
+ );
95
+
96
+ COMMENT ON TABLE public.sms_messages IS 'Stores SMS messages received for verification services.';
97
+
98
+ -- Settings table
99
+ CREATE TABLE IF NOT EXISTS public.settings (
100
+ id SERIAL PRIMARY KEY, -- Auto-incrementing primary key
101
+ key VARCHAR(50) UNIQUE NOT NULL, -- Setting key
102
+ value TEXT NOT NULL, -- Setting value
103
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When setting was created
104
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- When setting was last updated
105
+ );
106
+
107
+ COMMENT ON TABLE public.settings IS 'Stores system configuration settings.';
108
+
109
+ -- Trigger for updating timestamps
110
+ CREATE TRIGGER update_settings_timestamp BEFORE UPDATE ON public.settings
111
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
112
+
113
+ -- Add bots table if it doesn't exist
114
+ CREATE TABLE IF NOT EXISTS public.bots (
115
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
116
+ name VARCHAR(255) NOT NULL,
117
+ user_id UUID NOT NULL,
118
+ bot_token VARCHAR(255) NOT NULL UNIQUE,
119
+ is_active BOOLEAN NOT NULL DEFAULT true,
120
+ currency VARCHAR(10) NOT NULL DEFAULT 'USD',
121
+ profit_type VARCHAR(10) NOT NULL DEFAULT 'percentage',
122
+ profit_value_percentage DECIMAL(5,2) DEFAULT 0,
123
+ profit_value_fix DECIMAL(10,2) DEFAULT 0,
124
+ last_activity TIMESTAMPTZ,
125
+ version VARCHAR(20) NOT NULL DEFAULT '1.0.0',
126
+
127
+ -- API Keys and External Service Configuration
128
+ fivesim_api_key VARCHAR(255),
129
+ paypal_client_id VARCHAR(255),
130
+ paypal_client_secret VARCHAR(255),
131
+ crypto_wallet_address VARCHAR(255),
132
+ admin_contact VARCHAR(255),
133
+
134
+ -- Group Join Settings
135
+ join_group_required BOOLEAN DEFAULT false,
136
+ group_channel_username VARCHAR(255),
137
+
138
+ settings JSONB DEFAULT '{}',
139
+ state JSONB DEFAULT '{}',
140
+ suffix_email VARCHAR(255) DEFAULT 'saerosms.com',
141
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
142
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
143
+ );
144
+
145
+ COMMENT ON TABLE public.bots IS 'Stores bot configurations and settings';
146
+
147
+ -- Enable Row Level Security
148
+ ALTER TABLE public.bots ENABLE ROW LEVEL SECURITY;
149
+
150
+ -- Trigger for updating timestamps
151
+ CREATE TRIGGER update_bots_timestamp BEFORE UPDATE ON public.bots
152
+ FOR EACH ROW EXECUTE FUNCTION update_timestamp();
153
+
src/bots/handlers/balanceHandlers.ts ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { authCheckHandler } from "../utils/handlerUtils";
12
+
13
+ const logger = createLogger('BalanceHandlers');
14
+ const authService = AuthService.getInstance();
15
+
16
+ // Add state management for custom amount
17
+ const customAmountStates = new Map<number, boolean>();
18
+
19
+ // Add payment method states
20
+ const paymentMethodStates = new Map<number, PaymentMethod>();
21
+
22
+ export const handleTopUpAction = authCheckHandler(async (ctx: BotContext) => {
23
+ const telegramId = ctx.from?.id;
24
+ const user = await authService.getUserByTelegramId(telegramId!, ctx);
25
+
26
+ if (!user) {
27
+ return {
28
+ message: messageManager.getMessage('user_not_found'),
29
+ options: getMainMenuKeyboard()
30
+ };
31
+ }
32
+
33
+ return {
34
+ message: messageManager.getMessage('balance_top_up_title') + '\n\n' +
35
+ messageManager.getMessage('balance_current_balance')
36
+ .replace('{balance}', user.balance?.toString() || '0') + '\n\n' +
37
+ messageManager.getMessage('balance_choose_payment_method'),
38
+ options: {
39
+ ...Markup.inlineKeyboard([
40
+ [Markup.button.callback(messageManager.getMessage('btn_payment_paypal'), 'payment_paypal')],
41
+ [Markup.button.callback(messageManager.getMessage('btn_payment_crypto'), 'payment_crypto')],
42
+ [Markup.button.callback(messageManager.getMessage('btn_payment_admin'), 'payment_admin')],
43
+ [Markup.button.callback(messageManager.getMessage('btn_back_to_main'), 'main_menu')]
44
+ ]),
45
+ parse_mode: 'HTML'
46
+ }
47
+ };
48
+ });
49
+
50
+ export const handleCustomAmountRequest = authCheckHandler(async (ctx: BotContext) => {
51
+ const telegramId = ctx.from?.id;
52
+ customAmountStates.set(telegramId!, true);
53
+
54
+ return {
55
+ message: messageManager.getMessage('balance_custom_amount_title') + '\n\n' +
56
+ messageManager.getMessage('balance_enter_custom_amount'),
57
+ options: {
58
+ ...Markup.inlineKeyboard([
59
+ [Markup.button.callback(messageManager.getMessage('btn_cancel'), 'top_up_balance')]
60
+ ]),
61
+ parse_mode: 'HTML'
62
+ }
63
+ };
64
+ });
65
+
66
+ export const handlePaymentMethodSelection = authCheckHandler(async (ctx: BotContext, method: PaymentMethod) => {
67
+ const telegramId = ctx.from?.id;
68
+ paymentMethodStates.set(telegramId!, method);
69
+
70
+ return {
71
+ message: messageManager.getMessage('balance_top_up_title') + '\n\n' +
72
+ messageManager.getMessage('balance_choose_amount'),
73
+ options: {
74
+ ...Markup.inlineKeyboard([
75
+ [
76
+ Markup.button.callback(messageManager.getMessage('btn_amount_5'), 'top_up_5'),
77
+ Markup.button.callback(messageManager.getMessage('btn_amount_10'), 'top_up_10'),
78
+ Markup.button.callback(messageManager.getMessage('btn_amount_20'), 'top_up_20')
79
+ ],
80
+ [
81
+ Markup.button.callback(messageManager.getMessage('btn_amount_50'), 'top_up_50'),
82
+ Markup.button.callback(messageManager.getMessage('btn_amount_100'), 'top_up_100')
83
+ ],
84
+ [Markup.button.callback(messageManager.getMessage('btn_custom_amount'), 'top_up_custom')],
85
+ [Markup.button.callback(messageManager.getMessage('btn_back_to_balance'), 'top_up_balance')]
86
+ ]),
87
+ parse_mode: 'HTML'
88
+ }
89
+ };
90
+ });
91
+
92
+ export const handleTopUpAmount = authCheckHandler(async (ctx: BotContext, amount: number) => {
93
+ const telegramId = ctx.from?.id;
94
+ const user = await authService.getUserByTelegramId(telegramId!, ctx);
95
+ const paymentMethod = paymentMethodStates.get(telegramId!);
96
+
97
+ if (!user) {
98
+ return {
99
+ message: messageManager.getMessage('user_not_found'),
100
+ options: getMainMenuKeyboard()
101
+ };
102
+ }
103
+
104
+ try {
105
+ let message: string;
106
+
107
+ switch (paymentMethod) {
108
+ case 'PAYPAL':
109
+ const paypalService = PayPalService.getInstance(ctx);
110
+ const paymentLink = await paypalService.createPaymentLink(user.id.toString(), amount);
111
+ message = messageManager.getMessage('payment_paypal_title') + '\n\n' +
112
+ `المبلغ المطلوب: $${amount}\n\n` +
113
+ messageManager.getMessage('payment_paypal_instructions')
114
+ .replace('{payment_link}', paymentLink);
115
+ break;
116
+
117
+ case 'CRYPTO':
118
+ const cryptoService = CryptoService.getInstance(ctx);
119
+ const cryptoAddress = await cryptoService.getPaymentAddress(user.id.toString(), amount);
120
+ message = messageManager.getMessage('payment_crypto_title') + '\n\n' +
121
+ `المبلغ المطلوب: $${amount}\n\n` +
122
+ messageManager.getMessage('payment_crypto_instructions')
123
+ .replace('{wallet_address}', cryptoAddress);
124
+ break;
125
+
126
+ case 'ADMIN':
127
+ const adminService = AdminService.getInstance(ctx);
128
+ const adminContact = await adminService.getAdminContact();
129
+ message = messageManager.getMessage('payment_admin_title') + '\n\n' +
130
+ `المبلغ المطلوب: $${amount}\n\n` +
131
+ messageManager.getMessage('payment_admin_instructions')
132
+ .replace('{admin_contact}', adminContact);
133
+ break;
134
+
135
+ default:
136
+ throw new Error('Invalid payment method');
137
+ }
138
+
139
+ return {
140
+ message: message,
141
+ options: {
142
+ ...Markup.inlineKeyboard([
143
+ [Markup.button.callback(messageManager.getMessage('btn_back_to_main'), 'main_menu')]
144
+ ]),
145
+ parse_mode: 'HTML'
146
+ }
147
+ };
148
+ } catch (error) {
149
+ logger.error('Error creating payment:', error);
150
+ return {
151
+ message: messageManager.getMessage('payment_error'),
152
+ options: getLoggedInMenuKeyboard()
153
+ };
154
+ }
155
+ });
156
+
157
+ // Add handler for custom amount input
158
+ export const handleCustomAmountInput = async (ctx: BotContext) => {
159
+ const telegramId = ctx.from?.id;
160
+
161
+ if (!telegramId || !customAmountStates.get(telegramId)) {
162
+ return;
163
+ }
164
+
165
+ const message = ctx.message;
166
+ const amountText = (message && 'text' in message) ? message.text : undefined;
167
+ if (!amountText) return;
168
+
169
+ // Remove state
170
+ customAmountStates.delete(telegramId)
171
+
172
+ // Parse amount
173
+ const amount = parseFloat(amountText);
174
+ if (isNaN(amount) || amount <= 0) {
175
+ return {
176
+ message: "⚠️ المبلغ غير صالح. الرجاء إدخال رقم صحيح أو عشري موجب.",
177
+ options: {
178
+ ...Markup.inlineKeyboard([
179
+ [Markup.button.callback('🔙 العودة', 'top_up_balance')]
180
+ ]),
181
+ parse_mode: 'HTML'
182
+ }
183
+ };
184
+ }
185
+
186
+ // Handle the custom amount
187
+ return await handleTopUpAmount(ctx, amount);
188
+ };
189
+
190
+ export const setupBalanceHandlers = (bot: any) => {
191
+ // Handle top-up button click
192
+ bot.action('top_up_balance', async (ctx: BotContext) => {
193
+ const result = await handleTopUpAction(ctx);
194
+ await ctx.editMessageText(result.message, result.options);
195
+ });
196
+
197
+ // Add custom amount handler
198
+ bot.action('top_up_custom', async (ctx: BotContext) => {
199
+ const result = await handleCustomAmountRequest(ctx);
200
+ await ctx.editMessageText(result.message, result.options);
201
+ });
202
+
203
+ // Handle text messages for custom amount
204
+ bot.on('text', async (ctx: BotContext) => {
205
+ const result = await handleCustomAmountInput(ctx);
206
+ if (result) {
207
+ await ctx.reply(result.message, result.options);
208
+ }
209
+ });
210
+
211
+ // Handle specific amount selections
212
+ bot.action('top_up_5', async (ctx: BotContext) => {
213
+ const result = await handleTopUpAmount(ctx, 5);
214
+ await ctx.editMessageText(result.message, result.options);
215
+ });
216
+
217
+ bot.action('top_up_10', async (ctx: BotContext) => {
218
+ const result = await handleTopUpAmount(ctx, 10);
219
+ await ctx.editMessageText(result.message, result.options);
220
+ });
221
+
222
+ bot.action('top_up_20', async (ctx: BotContext) => {
223
+ const result = await handleTopUpAmount(ctx, 20);
224
+ await ctx.editMessageText(result.message, result.options);
225
+ });
226
+
227
+ bot.action('top_up_50', async (ctx: BotContext) => {
228
+ const result = await handleTopUpAmount(ctx, 50);
229
+ await ctx.editMessageText(result.message, result.options);
230
+ });
231
+
232
+ bot.action('top_up_100', async (ctx: BotContext) => {
233
+ const result = await handleTopUpAmount(ctx, 100);
234
+ await ctx.editMessageText(result.message, result.options);
235
+ });
236
+
237
+ // Add payment method handlers
238
+ bot.action('payment_paypal', async (ctx: BotContext) => {
239
+ const result = await handlePaymentMethodSelection(ctx, 'PAYPAL');
240
+ await ctx.editMessageText(result.message, result.options);
241
+ });
242
+
243
+ bot.action('payment_crypto', async (ctx: BotContext) => {
244
+ const result = await handlePaymentMethodSelection(ctx, 'CRYPTO');
245
+ await ctx.editMessageText(result.message, result.options);
246
+ });
247
+
248
+ bot.action('payment_admin', async (ctx: BotContext) => {
249
+ const result = await handlePaymentMethodSelection(ctx, 'ADMIN');
250
+ await ctx.editMessageText(result.message, result.options);
251
+ });
252
+ };
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,ctx)) {
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,ctx);
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).toString()),
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,ctx)) {
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,ctx)) {
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,ctx);
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,ctx)) {
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,ctx)) {
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,ctx)) {
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,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import type { ParseMode } from 'telegraf/types';
11
+
12
+ export const setupCallbackHandlers = (bot: any) => {
13
+ setupMainMenuHandlers(bot);
14
+ setupServiceHandlers(bot);
15
+ setupPurchaseHandlers(bot);
16
+ setupHistoryHandlers(bot);
17
+ setupBalanceHandlers(bot);
18
+ setupLanguageHandlers(bot);
19
+ setupProfileHandlers(bot);
20
+
21
+ bot.action('gift_balance', async (ctx: BotContext) => {
22
+ const result = await handleGiftBalanceAction(ctx);
23
+ await ctx.editMessageText(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
24
+ });
25
+
26
+ // Handle text messages for gift process
27
+ bot.on('text', async (ctx: BotContext) => {
28
+ const telegramId = ctx.from?.id;
29
+ if (!telegramId) return;
30
+
31
+ const giftState = giftStates.get(telegramId);
32
+ if (!giftState) return;
33
+
34
+ let result;
35
+ if (giftState.step === 1) {
36
+ result = await handleGiftEmailInput(ctx);
37
+ } else if (giftState.step === 2) {
38
+ result = await handleGiftAmountInput(ctx);
39
+ }
40
+
41
+ if (result) {
42
+ await ctx.reply(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
43
+ }
44
+ });
45
+ };
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,ctx) ;
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,ctx) : 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,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,ctx)) {
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,ctx );
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: {
68
+ ...getLoggedInMenuKeyboard(),
69
+ parse_mode: 'HTML'
70
+ }
71
+ };
72
+ } else {
73
+ try {
74
+
75
+
76
+ const { user: newUser, password } = await authService.createUser(telegramId, firstName, ctx);
77
+ authService.setUserLoggedIn(telegramId,ctx, true);
78
+
79
+
80
+ return {
81
+ message: messageManager.getMessage('account_created_success')
82
+ .replace('{firstName}', firstName)
83
+ .replace('{email}', newUser.email)
84
+ .replace('{password}', password),
85
+ options: getLoggedInMenuKeyboard()
86
+ };
87
+ } catch (error: any) {
88
+ logger.error(`Error creating new user: ${error.message}`);
89
+ return {
90
+ message: messageManager.getMessage('login_error_create_account'),
91
+ options: getBackToMainMenuButton()
92
+ };
93
+ }
94
+ }
95
+ } catch (error: any) {
96
+ logger.error(`Error in login action: ${error.message}`);
97
+ return {
98
+ message: messageManager.getMessage('login_error_general'),
99
+ options: getBackToMainMenuButton()
100
+ };
101
+ }
102
+ };
103
+ export const handleTermsAction = async (ctx: BotContext) => {
104
+ return {
105
+ message: getTermsMessage(),
106
+ options: { parse_mode: 'HTML' }
107
+ };
108
+ };
109
+
110
+ export const handleNewMembersAction = async (ctx: BotContext) => {
111
+ return {
112
+ message: getNewMembersMessage(),
113
+ options: { parse_mode: 'HTML' }
114
+ };
115
+ };
116
+
117
+ export const handleStatsAction = async (ctx: BotContext) => {
118
+ return {
119
+ message: getStatsMessage(),
120
+ options: { parse_mode: 'HTML' }
121
+ };
122
+ };
123
+
124
+ export const handleMainMenuAction = async (ctx: BotContext) => {
125
+ const name = ctx.from?.first_name || messageManager.getMessage('default_user_name');
126
+ const telegramId = ctx.from?.id;
127
+
128
+ if (telegramId && authService.isUserLoggedIn(telegramId,ctx)) {
129
+ return {
130
+ message: messageManager.getMessage('main_menu_welcome_back').replace('{name}', name),
131
+ options: getLoggedInMenuKeyboard()
132
+ };
133
+ } else {
134
+ return {
135
+ message: getWelcomeMessage(name),
136
+ options: getMainMenuKeyboard()
137
+ };
138
+ }
139
+ };
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,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from "../types/botTypes";
2
+ import { Markup } from "telegraf";
3
+ import type { ParseMode } from "telegraf/types";
4
+ import { getProfileKeyboard, getLoggedInMenuKeyboard, getMainMenuKeyboard } from "../utils/keyboardUtils";
5
+ import { authService } from "../services/auth";
6
+ import { supabase } from "../../db/supabase";
7
+ import { createLogger } from "../../utils/logger";
8
+ import { messageReplyHandler } from "../utils/handlerUtils";
9
+
10
+ const logger = createLogger('ProfileHandlers');
11
+
12
+ // State management for profile actions
13
+ const profileStates = new Map<number, {
14
+ action: 'change_email' | 'change_password' | null;
15
+ step: number;
16
+ }>();
17
+
18
+ export const setupProfileHandlers = (bot: any) => {
19
+ // Profile menu handlers
20
+ bot.action('profile', async (ctx: BotContext) => {
21
+ const result = await handleProfileAction(ctx);
22
+ await ctx.editMessageText(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
23
+ });
24
+
25
+ bot.action('account_info', async (ctx: BotContext) => {
26
+ const result = await handleAccountInfoAction(ctx);
27
+ await ctx.editMessageText(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
28
+ });
29
+
30
+ bot.action('change_email', async (ctx: BotContext) => {
31
+ const result = await handleChangeEmailAction(ctx);
32
+ await ctx.editMessageText(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
33
+ });
34
+
35
+ bot.action('change_password', async (ctx: BotContext) => {
36
+ const result = await handleChangePasswordAction(ctx);
37
+ await ctx.editMessageText(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
38
+ });
39
+
40
+ // Message handlers for input processing
41
+ bot.on('text', async (ctx: BotContext) => {
42
+ const telegramId = ctx.from?.id;
43
+ if (!telegramId) return;
44
+
45
+ const userState = profileStates.get(telegramId);
46
+ if (!userState) return;
47
+
48
+ let result;
49
+ switch (userState.action) {
50
+ case 'change_email':
51
+ result = await handleEmailInput(ctx);
52
+ break;
53
+ case 'change_password':
54
+ result = await handlePasswordInput(ctx);
55
+ break;
56
+ }
57
+
58
+ if (result) {
59
+ await ctx.reply(result.message, { ...result.options, parse_mode: 'HTML' as ParseMode });
60
+ }
61
+ });
62
+ };
63
+
64
+ const handleProfileAction = async (ctx: BotContext) => {
65
+ const telegramId = ctx.from?.id;
66
+
67
+ if (!telegramId || !authService.isUserLoggedIn(telegramId,ctx)) {
68
+ return {
69
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
70
+ options: getMainMenuKeyboard()
71
+ };
72
+ }
73
+
74
+ const user = await authService.getUserByTelegramId(telegramId,ctx);
75
+
76
+ if (!user) {
77
+ return {
78
+ message: "⚠️ حدث خطأ في تحديد المستخدم",
79
+ options: getMainMenuKeyboard()
80
+ };
81
+ }
82
+
83
+ return {
84
+ message: `👤 <b>الملف الشخصي</b>\n\n` +
85
+ `اختر الإجراء الذي تريد تنفيذه:`,
86
+ options: {
87
+ ...getProfileKeyboard(),
88
+ parse_mode: 'HTML'
89
+ }
90
+ };
91
+ };
92
+
93
+ const handleAccountInfoAction = async (ctx: BotContext) => {
94
+ const telegramId = ctx.from?.id;
95
+
96
+ if (!telegramId || !authService.isUserLoggedIn(telegramId,ctx)) {
97
+ return {
98
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
99
+ options: getMainMenuKeyboard()
100
+ };
101
+ }
102
+
103
+ const user = await authService.getUserByTelegramId(telegramId,ctx);
104
+
105
+ if (!user) {
106
+ return {
107
+ message: "⚠️ حدث خطأ في تحديد المستخدم",
108
+ options: getMainMenuKeyboard()
109
+ };
110
+ }
111
+
112
+ return {
113
+ message: `👤 <b>معلومات الحساب</b>\n\n` +
114
+ `الاسم: ${user.firstName}\n` +
115
+ `البريد الإلكتروني: ${user.email}\n` +
116
+ `الرصيد: ${user.balance || 0}$\n` +
117
+ `تاريخ التسجيل: ${new Date(user.createdAt).toLocaleDateString()}`,
118
+ options: {
119
+ ...getProfileKeyboard(),
120
+ parse_mode: 'HTML'
121
+ }
122
+ };
123
+ };
124
+
125
+ const handleChangeEmailAction = async (ctx: BotContext) => {
126
+ const telegramId = ctx.from?.id;
127
+
128
+ if (!telegramId || !authService.isUserLoggedIn(telegramId,ctx)) {
129
+ return {
130
+ message: "⚠️ الرجاء تسجيل الدخول أولاً",
131
+ options: getMainMenuKeyboard()
132
+ };
133
+ }
134
+
135
+ // Set user state for email change
136
+ profileStates.set(telegramId, { action: 'change_email', step: 1 });
137
+ logger.info(`Set profile state for ${telegramId}:`, profileStates.get(telegramId));
138
+
139
+ return {
140
+ message: "📧 <b>تغيير البريد الإلكتروني</b>\n\n" +
141
+ "الرجاء إدخال البريد الإلكتروني الجديد:\n" +
142
+ "يجب أن يكون البريد الإلكتروني صالحاً ويحتوي على @",
143
+ options: {
144
+ ...getProfileKeyboard(),
145
+ parse_mode: 'HTML'
146
+ }
147
+ };
148
+ };
149
+
150
+ const handleChangePasswordAction = async (ctx: BotContext) => {
151
+ const telegramId = ctx.from?.id;
152
+
153
+ if (!telegramId || !authService.isUserLoggedIn(telegramId,ctx)) {
154
+ return {
155
+ message: "⚠️ الر��اء تسجيل الدخول أولاً",
156
+ options: getMainMenuKeyboard()
157
+ };
158
+ }
159
+
160
+ // Set user state for password change
161
+ profileStates.set(telegramId, { action: 'change_password', step: 1 });
162
+
163
+ return {
164
+ message: "🔑 <b>تغيير كلمة المرور</b>\n\n" +
165
+ "الرجاء إدخال كلمة المرور الجديدة:\n" +
166
+ "يجب أن تكون كلمة المرور 8 أحرف على الأقل وتحتوي على:\n" +
167
+ "- حرف كبير\n" +
168
+ "- حرف صغير\n" +
169
+ "- رقم\n" +
170
+ "- رمز خاص",
171
+ options: {
172
+ ...getProfileKeyboard(),
173
+ parse_mode: 'HTML'
174
+ }
175
+ };
176
+ };
177
+
178
+ // Input handlers
179
+ export const handleEmailInput = async (ctx: BotContext) => {
180
+ const telegramId = ctx.from?.id;
181
+
182
+ if (!telegramId || !profileStates.get(telegramId)) {
183
+ return;
184
+ }
185
+
186
+ // Use a type guard to check for text message
187
+ const message = ctx.message;
188
+ const email = (message && 'text' in message) ? message.text : undefined;
189
+ if (!email) return;
190
+
191
+ // Remove state
192
+ profileStates.delete(telegramId);
193
+
194
+ // Validate email
195
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
196
+ if (!emailRegex.test(email)) {
197
+ return {
198
+ message: "❌ البريد الإلكتروني غير صالح. الرجاء إدخال بريد إلكتروني صحيح.",
199
+ options: {
200
+ ...getProfileKeyboard(),
201
+ parse_mode: 'HTML'
202
+ }
203
+ };
204
+ }
205
+
206
+ try {
207
+ // Check if email is already in use
208
+ const { data: existingUser, error: checkError } = await supabase
209
+ .from('users_bot_telegram')
210
+ .select('id')
211
+ .eq('email', email)
212
+ .single();
213
+
214
+ if (checkError) {
215
+ logger.error(`Error checking existing email: ${checkError}`);
216
+ throw checkError;
217
+ }
218
+
219
+ if (existingUser) {
220
+ return {
221
+ message: "❌ هذا البريد الإلكتروني مستخدم بالفعل. الرجاء استخدام بريد إلكتروني آخر.",
222
+ options: {
223
+ ...getProfileKeyboard(),
224
+ parse_mode: 'HTML'
225
+ }
226
+ };
227
+ }
228
+
229
+ // Update email in database
230
+ const { error: updateError } = await supabase
231
+ .from('users_bot_telegram')
232
+ .update({ email })
233
+ .eq('telegram_id', telegramId);
234
+
235
+ if (updateError) {
236
+ logger.error(`Error updating email: ${updateError}`);
237
+ throw updateError;
238
+ }
239
+
240
+ return {
241
+ message: "✅ تم تغيير البريد الإلكتروني بنجاح!",
242
+ options: {
243
+ ...getLoggedInMenuKeyboard(),
244
+ parse_mode: 'HTML'
245
+ }
246
+ };
247
+ } catch (error) {
248
+ logger.error(`Error updating email for user ${telegramId}: ${error}`);
249
+ return {
250
+ message: "❌ حدث خطأ أثناء تحديث البريد الإلكتروني. الرجاء المحاولة مرة أخرى.",
251
+ options: {
252
+ ...getProfileKeyboard(),
253
+ parse_mode: 'HTML'
254
+ }
255
+ };
256
+ }
257
+ };
258
+
259
+ export const handlePasswordInput = async (ctx: BotContext) => {
260
+ const telegramId = ctx.from?.id;
261
+
262
+ if (!telegramId || !profileStates.get(telegramId)) {
263
+ return;
264
+ }
265
+
266
+ // Use a type guard to check for text message
267
+ const message = ctx.message;
268
+ const password = (message && 'text' in message) ? message.text : undefined;
269
+ if (!password) return;
270
+
271
+ // Remove state
272
+ profileStates.delete(telegramId);
273
+
274
+ // Validate password
275
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
276
+ if (!passwordRegex.test(password)) {
277
+ return {
278
+ message: "❌ كلمة المرور غير صالحة. الرجاء التأكد من أن كلمة المرور:\n" +
279
+ "- 8 أحرف على الأقل\n" +
280
+ "- تحتوي على حرف كبير\n" +
281
+ "- تحتوي على حرف صغير\n" +
282
+ "- تحتوي على رقم\n" +
283
+ "- تحتوي على رمز خاص",
284
+ options: {
285
+ ...getProfileKeyboard(),
286
+ parse_mode: 'HTML'
287
+ }
288
+ };
289
+ }
290
+
291
+ try {
292
+ // Hash the new password
293
+ const passwordHash = await authService.hashPassword(password);
294
+
295
+ // Update password in database
296
+ const { error } = await supabase
297
+ .from('users_bot_telegram')
298
+ .update({ passwordHash })
299
+ .eq('telegram_id', telegramId);
300
+
301
+ if (error) throw error;
302
+
303
+ return {
304
+ message: "✅ تم تغيير كلمة المرور بنجاح!",
305
+ options: {
306
+ ...getLoggedInMenuKeyboard(),
307
+ parse_mode: 'HTML'
308
+ }
309
+ };
310
+ } catch (error) {
311
+ logger.error(`Error updating password for user ${telegramId}: ${error}`);
312
+ return {
313
+ message: "❌ حدث خطأ أثناء تحديث كلمة المرور. الرجاء المحاولة مرة أخرى.",
314
+ options: {
315
+ ...getProfileKeyboard(),
316
+ parse_mode: 'HTML'
317
+ }
318
+ };
319
+ }
320
+ };
src/bots/handlers/purchaseHandlers.ts ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {
7
+ getAuthRequiredMessage,
8
+ getServiceUnavailableMessage,
9
+ getInsufficientBalanceMessage,
10
+ getPurchaseSuccessMessage,
11
+ getPurchaseFailedMessage,
12
+ getPurchaseErrorMessage,
13
+ getVerificationTimeoutMessage,
14
+ getProcessingPurchaseMessage
15
+ } from "../utils/messageUtils";
16
+ import { getLoggedInMenuKeyboard, getMainMenuKeyboard } from "../utils/keyboardUtils";
17
+ import { messageManager } from "../utils/messageManager";
18
+ import { calculatePriceWithProfit } from "../utils/priceUtils";
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, 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
+ loadingMsgId: number
55
+ ) => {
56
+ // 🔒 Authentication check
57
+ const telegramId = ctx.from?.id;
58
+ if (!telegramId || !authService.isUserLoggedIn(telegramId,ctx)) {
59
+ await ctx.deleteMessage(loadingMsgId);
60
+ await ctx.reply(getAuthRequiredMessage(), getMainMenuKeyboard());
61
+ return;
62
+ }
63
+
64
+ let costWithProfit = 0;
65
+ try {
66
+ const prices = await virtualNumberService.getPrices(service, countryId);
67
+
68
+ // Add validation to check if the price exists for this operator
69
+ if (!prices || !prices[countryId] || !prices[countryId][service] || !prices[countryId][service][operator]) {
70
+ await ctx.deleteMessage(loadingMsgId);
71
+ await ctx.reply(
72
+ getServiceUnavailableMessage(service, operator, countryId),
73
+ getLoggedInMenuKeyboard()
74
+ );
75
+ return;
76
+ }
77
+
78
+ const cost = prices[countryId][service][operator].cost;
79
+
80
+ // Apply profit to the cost
81
+ costWithProfit = calculatePriceWithProfit(ctx, cost, 'RUB');
82
+
83
+ // 💰 Balance check
84
+ const user = await authService.getUserByTelegramId(telegramId,ctx);
85
+ if (!user || user.balance < costWithProfit) {
86
+ await ctx.deleteMessage(loadingMsgId);
87
+ await ctx.reply(
88
+ getInsufficientBalanceMessage(costWithProfit, user?.balance || 0),
89
+ getLoggedInMenuKeyboard()
90
+ );
91
+ return;
92
+ }
93
+
94
+ // 🛒 Purchase number
95
+ const purchaseResult = await virtualNumberService.purchaseNumber(service, countryId, operator);
96
+ await ctx.deleteMessage(loadingMsgId);
97
+
98
+ if (purchaseResult && purchaseResult.phone) {
99
+ // ➖ Deduct balance
100
+ await authService.updateUserBalance(telegramId,ctx, user.balance - costWithProfit);
101
+
102
+ // 💾 Create purchase record in database with PENDING state
103
+ const purchaseRecord = await purchaseTrackingService.createPurchase({
104
+ userId: user.id,
105
+ telegramId: telegramId,
106
+ service: service,
107
+ countryId: countryId,
108
+ operator: operator,
109
+ phoneNumber: purchaseResult.phone,
110
+ orderId: purchaseResult.id,
111
+ cost: costWithProfit,
112
+ state: PurchaseState.PENDING
113
+ });
114
+
115
+ // Send purchase confirmation
116
+ const confirmMsg = await ctx.reply(
117
+ getPurchaseSuccessMessage(purchaseResult, service, countryId, operator, costWithProfit),
118
+ getLoggedInMenuKeyboard()
119
+ );
120
+
121
+
122
+ // Start checking for verification code
123
+ const orderId = purchaseResult.id;
124
+ const maxAttempts = 30; // Check for up to 15 minutes (30 × 30s = 15min)
125
+ let attempts = 0;
126
+
127
+ const checkInterval = setInterval(async () => {
128
+ try {
129
+ attempts++;
130
+
131
+ // Check if we've reached max attempts
132
+ if (attempts >= maxAttempts) {
133
+ clearInterval(checkInterval);
134
+
135
+ // Update purchase record to TIMEOUT state
136
+ await purchaseTrackingService.updatePurchaseState(
137
+ orderId,
138
+ PurchaseState.TIMEOUT
139
+ );
140
+
141
+ // Return the cost to user's balance on timeout
142
+ await authService.updateUserBalance(telegramId,ctx, user.balance + costWithProfit);
143
+
144
+ await ctx.reply(
145
+ getVerificationTimeoutMessage(),
146
+ getLoggedInMenuKeyboard()
147
+ );
148
+ return;
149
+ }
150
+
151
+ // Check for SMS
152
+ const smsResult = await virtualNumberService.checkSMS(orderId);
153
+
154
+ // If SMS received
155
+ if (smsResult && smsResult.sms && smsResult.sms.length > 0) {
156
+ clearInterval(checkInterval);
157
+
158
+ // Get the latest SMS
159
+ const latestSMS = smsResult.sms[smsResult.sms.length - 1];
160
+
161
+ // Extract verification code (this is a simple pattern, might need adjustment)
162
+ let verificationCode = latestSMS.text;
163
+
164
+ // Try to extract numeric code if it exists
165
+ const codeMatch = verificationCode.match(/\b\d{4,8}\b/);
166
+ if (codeMatch) {
167
+ verificationCode = `${codeMatch[0]} (${verificationCode})`;
168
+ }
169
+
170
+ // Create verification message
171
+ const verificationMessage = messageManager.getMessage('verification_received')
172
+ .replace('{phone}', purchaseResult.phone)
173
+ .replace('{code}', verificationCode)
174
+ .replace('{time}', new Date().toLocaleTimeString('ar-SA'));
175
+
176
+ // Update purchase record to SUCCESS state
177
+ await purchaseTrackingService.updatePurchaseState(
178
+ orderId,
179
+ PurchaseState.SUCCESS,
180
+ {
181
+ verificationCode: verificationCode,
182
+ verificationMessage: verificationMessage
183
+ }
184
+ );
185
+
186
+ await ctx.reply(
187
+ verificationMessage,
188
+ getLoggedInMenuKeyboard()
189
+ );
190
+ }
191
+ } catch (error: any) {
192
+ logger.error(`Error checking SMS for order ${orderId}: ${error.message}`);
193
+ // Don't stop checking on error, just log it
194
+ }
195
+ }, 30000); // Check every 30 seconds
196
+
197
+ } else {
198
+ // Create purchase record with FAILED state
199
+ if (user) {
200
+ await purchaseTrackingService.createPurchase({
201
+ userId: user.id,
202
+ telegramId: telegramId,
203
+ service: service,
204
+ countryId: countryId,
205
+ operator: operator,
206
+ cost: costWithProfit,
207
+ state: PurchaseState.FAILED
208
+ });
209
+ }
210
+
211
+ await ctx.reply(getPurchaseFailedMessage(), getLoggedInMenuKeyboard());
212
+ }
213
+
214
+ } catch (error: any) {
215
+ logger.error(`Error purchasing number for ${service} in ${countryId}: ${error.message}`);
216
+
217
+ // Create purchase record with FAILED state if we have user info
218
+ // try {
219
+ // const user = await authService.getUserByTelegramId(telegramId!,ctx);
220
+ // if (user) {
221
+ // // Return the cost to user's balance on error
222
+ // await authService.updateUserBalance(telegramId!,ctx, user.balance + costWithProfit);
223
+
224
+ // await purchaseTrackingService.createPurchase({
225
+ // userId: user.id,
226
+ // telegramId: telegramId!,
227
+ // service: service,
228
+ // countryId: countryId,
229
+ // operator: operator,
230
+ // cost: costWithProfit,
231
+ // state: PurchaseState.FAILED
232
+ // });
233
+ // }
234
+ // } catch (dbError) {
235
+ // logger.error(`Error creating failed purchase record: ${dbError}`);
236
+ // }
237
+
238
+ await ctx.deleteMessage(loadingMsgId);
239
+ await ctx.reply(getPurchaseErrorMessage(error.message), getLoggedInMenuKeyboard());
240
+ }
241
+ };
src/bots/handlers/serviceHandlers.ts ADDED
@@ -0,0 +1,878 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ getSearchProductPromptMessage,
14
+ getNoSearchResultsMessage,
15
+ getInvalidSearchInputMessage,
16
+ getSearchCountryPromptMessage,
17
+ getNoCountrySearchResultsMessage,
18
+ getNoAffordableProductsMessage,
19
+ getAffordableProductsMessage
20
+ } from "../utils/messageUtils";
21
+ import {
22
+ getLoggedInMenuKeyboard,
23
+ getMainMenuKeyboard,
24
+ getCountriesKeyboard,
25
+ getServicePricesKeyboard,
26
+ getServicesKeyboard,
27
+ getServicesPaginationInfo
28
+ } from "../utils/keyboardUtils";
29
+ import { calculatePriceWithProfit, formatPrice } from "../utils/priceUtils";
30
+ import fiveSimProducts from "../utils/5sim_products.json";
31
+ import { countryData } from "../utils/country";
32
+ import { Markup } from 'telegraf';
33
+ import { Message } from 'telegraf/typings/core/types/typegram';
34
+
35
+ const logger = createLogger('ServiceHandlers');
36
+ const authService = AuthService.getInstance();
37
+ const virtualNumberService = VirtualNumberService.getInstance();
38
+
39
+ // Temporary in-memory state to track if a user is searching for a product
40
+ interface UserSearchState {
41
+ waitingForProductSearch?: boolean;
42
+ waitingForCountrySearch?: { // If searching for a country
43
+ serviceId: string;
44
+ sortBy?: 'price_asc'; // Preserve sorting preference
45
+ page: number; // Preserve current page
46
+ };
47
+ }
48
+ const userSearchStates = new Map<number, UserSearchState>();
49
+
50
+ export const setupServiceHandlers = (bot: any) => {
51
+ // Add handler for browse services button
52
+ bot.action('browse_services', async (ctx: BotContext) => {
53
+ await ctx.answerCbQuery();
54
+ await handleBrowseServices(ctx, undefined); // Pass undefined for initial sortBy
55
+ });
56
+
57
+ // Add handler for services pagination
58
+ bot.action(/^services_page_(\d+)(_([a-zA-Z]+))?$/, async (ctx: BotContext) => {
59
+ await ctx.answerCbQuery();
60
+ const match = ctx.match as RegExpMatchArray;
61
+ const page = parseInt(match[1], 10);
62
+ const sortBy = match[3] as 'az' | 'za' | undefined;
63
+ await handleServicesPagination(ctx, page, sortBy);
64
+ });
65
+
66
+ // Add handler for sorting services by A-Z
67
+ bot.action(/^sort_services_az_(\d+)$/, async (ctx: BotContext) => {
68
+ await ctx.answerCbQuery();
69
+ const match = ctx.match as RegExpMatchArray;
70
+ const page = parseInt(match[1], 10);
71
+ await handleServicesPagination(ctx, page, 'az');
72
+ });
73
+
74
+ // Add handler for sorting services by Z-A
75
+ bot.action(/^sort_services_za_(\d+)$/, async (ctx: BotContext) => {
76
+ await ctx.answerCbQuery();
77
+ const match = ctx.match as RegExpMatchArray;
78
+ const page = parseInt(match[1], 10);
79
+ await handleServicesPagination(ctx, page, 'za');
80
+ });
81
+
82
+ // Add handler for sorting countries by price
83
+ bot.action(/^sort_countries_price_asc_(.+)_(\d+)$/, async (ctx: BotContext) => {
84
+ await ctx.answerCbQuery();
85
+ const match = ctx.match as RegExpMatchArray;
86
+ const service = match[1];
87
+ const page = parseInt(match[2], 10);
88
+ await handleServiceSelection(ctx, service, 'price_asc', page);
89
+ });
90
+
91
+ // Add handler for country pagination
92
+ bot.action(/^country_page_(.+)_(\d+)$/, async (ctx: BotContext) => {
93
+ await ctx.answerCbQuery();
94
+ const match = ctx.match as RegExpMatchArray;
95
+ const service = match[1];
96
+ const page = parseInt(match[2], 10);
97
+ await handleServiceSelection(ctx, service, undefined, page);
98
+ });
99
+
100
+ // Add handler for search product button
101
+ bot.action('search_product', async (ctx: BotContext) => {
102
+ await ctx.answerCbQuery();
103
+ const telegramId = ctx.from?.id;
104
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
105
+ try {
106
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
107
+ } catch (editError: any) {
108
+ if (editError.message && editError.message.includes('message is not modified')) {
109
+ logger.info(`Auth required message not modified (search_product). No action needed.`);
110
+ } else {
111
+ logger.error(`Error editing auth required message (search_product): ${editError.message}`);
112
+ }
113
+ }
114
+ return;
115
+ }
116
+
117
+ userSearchStates.set(telegramId, { waitingForProductSearch: true });
118
+ try {
119
+ await ctx.editMessageText(getSearchProductPromptMessage(), {
120
+ reply_markup: Markup.inlineKeyboard([Markup.button.callback('↩️ Cancel Search', 'cancel_search')]).reply_markup
121
+ });
122
+ } catch (editError: any) {
123
+ if (editError.message && editError.message.includes('message is not modified')) {
124
+ logger.info(`Search product prompt not modified. No action needed.`);
125
+ } else {
126
+ logger.error(`Error editing search product prompt: ${editError.message}`);
127
+ }
128
+ }
129
+ });
130
+
131
+ // Add handler for search country button
132
+ bot.action(/^search_country_(.+)_([a-zA-Z0-9]+|default)_(\d+)$/, async (ctx: BotContext) => {
133
+ await ctx.answerCbQuery();
134
+ const telegramId = ctx.from?.id;
135
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
136
+ try {
137
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
138
+ } catch (editError: any) {
139
+ if (editError.message && editError.message.includes('message is not modified')) {
140
+ logger.info(`Auth required message not modified (search_country). No action needed.`);
141
+ } else {
142
+ logger.error(`Error editing auth required message (search_country): ${editError.message}`);
143
+ }
144
+ }
145
+ return;
146
+ }
147
+
148
+ const match = ctx.match as RegExpMatchArray;
149
+ const serviceId = match[1];
150
+ const sortBy = match[2] === 'default' ? undefined : match[2] as 'price_asc';
151
+ const page = parseInt(match[3], 10);
152
+
153
+ userSearchStates.set(telegramId, { waitingForCountrySearch: { serviceId, sortBy, page } });
154
+ try {
155
+ await ctx.editMessageText(getSearchCountryPromptMessage(), {
156
+ reply_markup: Markup.inlineKeyboard([Markup.button.callback('↩️ Cancel Search', `cancel_country_search_${serviceId}_${sortBy || 'default'}_${page}`)]).reply_markup
157
+ });
158
+ } catch (editError: any) {
159
+ if (editError.message && editError.message.includes('message is not modified')) {
160
+ logger.info(`Search country prompt not modified. No action needed.`);
161
+ } else {
162
+ logger.error(`Error editing search country prompt: ${editError.message}`);
163
+ }
164
+ }
165
+ });
166
+
167
+ // Add handler for canceling search
168
+ bot.action('cancel_search', async (ctx: BotContext) => {
169
+ await ctx.answerCbQuery();
170
+ const telegramId = ctx.from?.id;
171
+ if (telegramId) {
172
+ userSearchStates.delete(telegramId);
173
+ }
174
+ try {
175
+ await handleBrowseServices(ctx, undefined); // Pass undefined to reset sort
176
+ } catch (editError: any) {
177
+ if (editError.message && editError.message.includes('message is not modified')) {
178
+ logger.info(`Browse services message not modified (cancel_search). No action needed.`);
179
+ } else {
180
+ logger.error(`Error editing browse services message (cancel_search): ${editError.message}`);
181
+ }
182
+ }
183
+ });
184
+
185
+ // Add handler for canceling country search
186
+ bot.action(/^cancel_country_search_(.+)_([a-zA-Z0-9]+|default)_(\d+)$/, async (ctx: BotContext) => {
187
+ await ctx.answerCbQuery();
188
+ const telegramId = ctx.from?.id;
189
+ if (telegramId) {
190
+ userSearchStates.delete(telegramId);
191
+ }
192
+ const match = ctx.match as RegExpMatchArray;
193
+ const serviceId = match[1];
194
+ const sortBy = match[2] === 'default' ? undefined : match[2] as 'price_asc';
195
+ const page = parseInt(match[3], 10);
196
+ try {
197
+ await handleServiceSelection(ctx, serviceId, sortBy, page);
198
+ } catch (editError: any) {
199
+ if (editError.message && editError.message.includes('message is not modified')) {
200
+ logger.info(`Service selection message not modified (cancel_country_search). No action needed.`);
201
+ } else {
202
+ logger.error(`Error editing service selection message (cancel_country_search): ${editError.message}`);
203
+ }
204
+ }
205
+ });
206
+
207
+ // Add handler for 'buy_with_balance' button
208
+ bot.action('buy_with_balance', async (ctx: BotContext) => {
209
+ await ctx.answerCbQuery();
210
+ const telegramId = ctx.from?.id;
211
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
212
+ try {
213
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
214
+ } catch (editError: any) {
215
+ if (editError.message && editError.message.includes('message is not modified')) {
216
+ logger.info(`Auth required message not modified (buy_with_balance). No action needed.`);
217
+ } else {
218
+ logger.error(`Error editing auth required message (buy_with_balance): ${editError.message}`);
219
+ }
220
+ }
221
+ return;
222
+ }
223
+
224
+ try {
225
+ const user = await authService.getUserByTelegramId(telegramId, ctx);
226
+ if (!user) {
227
+ try {
228
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
229
+ } catch (editError: any) {
230
+ if (editError.message && editError.message.includes('message is not modified')) {
231
+ logger.info(`Auth required message not modified (buy_with_balance - user not found). No action needed.`);
232
+ } else {
233
+ logger.error(`Error editing auth required message (buy_with_balance - user not found): ${editError.message}`);
234
+ }
235
+ }
236
+ return;
237
+ }
238
+
239
+ const userBalance = user.balance;
240
+ const maxPrices = await virtualNumberService.getMaxPrices();
241
+
242
+ const affordableProducts = maxPrices.filter(product => {
243
+ // Convert product price to bot's currency (USD, assuming max-prices are in USD)
244
+ // And then apply profit calculation
245
+ const productPriceInBotCurrency = product.price;
246
+ const finalPrice = calculatePriceWithProfit(ctx, productPriceInBotCurrency, 'USD');
247
+ return userBalance >= finalPrice;
248
+ }).sort((a, b) => a.product.localeCompare(b.product)); // Sort alphabetically
249
+
250
+ if (affordableProducts.length === 0) {
251
+ try {
252
+ await ctx.editMessageText(getNoAffordableProductsMessage(userBalance, ctx), {
253
+ parse_mode: 'HTML',
254
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
255
+ });
256
+ } catch (editError: any) {
257
+ if (editError.message && editError.message.includes('message is not modified')) {
258
+ logger.info(`No affordable products message not modified. No action needed.`);
259
+ } else {
260
+ logger.error(`Error editing no affordable products message: ${editError.message}`);
261
+ }
262
+ }
263
+ return;
264
+ }
265
+
266
+ const buttons = [];
267
+ const rowSize = 2;
268
+
269
+ for (let i = 0; i < affordableProducts.length; i += rowSize) {
270
+ const row = [];
271
+ for (let j = 0; j < rowSize && i + j < affordableProducts.length; j++) {
272
+ const product = affordableProducts[i + j];
273
+ const serviceData = fiveSimProducts[product.product];
274
+ if (serviceData) {
275
+ // Apply profit to the price for display
276
+ const displayPrice = calculatePriceWithProfit(ctx, product.price, 'USD');
277
+ row.push(
278
+ Markup.button.callback(
279
+ `${serviceData.icon} ${serviceData.label_en} (${formatPrice(ctx, displayPrice)})`,
280
+ `service_${product.product}`
281
+ )
282
+ );
283
+ } else {
284
+ logger.warn(`Service data not found for product: ${product.product}`);
285
+ }
286
+ }
287
+ if (row.length > 0) {
288
+ buttons.push(row);
289
+ }
290
+ }
291
+
292
+ buttons.push([Markup.button.callback('🔙 Back to Menu', 'logged_in_menu')]);
293
+
294
+ try {
295
+ await ctx.editMessageText(getAffordableProductsMessage(userBalance, ctx), {
296
+ parse_mode: 'HTML',
297
+ reply_markup: Markup.inlineKeyboard(buttons).reply_markup
298
+ });
299
+ } catch (editError: any) {
300
+ if (editError.message && editError.message.includes('message is not modified')) {
301
+ logger.info(`Affordable products message not modified. No action needed.`);
302
+ } else {
303
+ logger.error(`Error editing affordable products message: ${editError.message}`);
304
+ }
305
+ }
306
+
307
+ } catch (error: any) {
308
+ logger.error(`Error handling buy_with_balance: ${error.message}`);
309
+ try {
310
+ await ctx.editMessageText('⚠️ An error occurred while fetching affordable products. Please try again later.', {
311
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
312
+ });
313
+ } catch (editError: any) {
314
+ if (editError.message && editError.message.includes('message is not modified')) {
315
+ logger.info(`Error message for affordable products not modified. No action needed.`);
316
+ } else {
317
+ logger.error(`Error editing error message for affordable products: ${editError.message}`);
318
+ }
319
+ }
320
+ }
321
+ });
322
+
323
+ // Text handler for product/country search input
324
+ bot.on('text', async (ctx: BotContext) => {
325
+ try {
326
+ const telegramId = ctx.from?.id;
327
+ const userState = userSearchStates.get(telegramId);
328
+
329
+ if (!telegramId || (!userState?.waitingForProductSearch && !userState?.waitingForCountrySearch)) {
330
+ return; // Not in search state, let other text handlers process or ignore
331
+ }
332
+
333
+ // Ensure ctx.message is a text message. Telegraf's `bot.on('text')` guarantees this, but TypeScript needs explicit narrowing.
334
+ const message = ctx.message as Message.TextMessage;
335
+
336
+ // Now 'text' property is safely accessible
337
+ const query = message.text?.toLowerCase().trim();
338
+
339
+ if (!query || query.length < 2) { // Require at least 2 characters for search
340
+ try {
341
+ await ctx.reply(getInvalidSearchInputMessage(), {
342
+ reply_markup: Markup.inlineKeyboard([Markup.button.callback('↩️ Cancel Search', userState.waitingForProductSearch ? 'cancel_search' : `cancel_country_search_${userState.waitingForCountrySearch?.serviceId}_${userState.waitingForCountrySearch?.sortBy || 'default'}_${userState.waitingForCountrySearch?.page}`)]).reply_markup
343
+ });
344
+ } catch (replyError: any) {
345
+ logger.error(`Error replying with invalid search input message: ${replyError.message}`);
346
+ }
347
+ return;
348
+ }
349
+
350
+ userSearchStates.delete(telegramId); // Clear search state after processing
351
+
352
+ if (userState.waitingForProductSearch) {
353
+ // Filter products based on search query
354
+ const searchResults = Object.entries(fiveSimProducts).filter(([serviceId, serviceData]) => {
355
+ return serviceId.toLowerCase().includes(query) || serviceData.label_en.toLowerCase().includes(query);
356
+ });
357
+
358
+ if (searchResults.length === 0) {
359
+ try {
360
+ await ctx.reply(getNoSearchResultsMessage(), {
361
+ reply_markup: Markup.inlineKeyboard([Markup.button.callback('🔙 Back to Services', 'browse_services')]).reply_markup
362
+ });
363
+ } catch (replyError: any) {
364
+ logger.error(`Error replying with no search results message: ${replyError.message}`);
365
+ }
366
+ return;
367
+ }
368
+
369
+ const buttons = [];
370
+ const rowSize = 2; // 2 buttons per row
371
+ const servicesPerPage = 20;
372
+
373
+ const currentPage = 0; // Always start search results on the first page
374
+
375
+ const startIndex = currentPage * servicesPerPage;
376
+ const endIndex = Math.min(startIndex + servicesPerPage, searchResults.length);
377
+ const pageServices = searchResults.slice(startIndex, endIndex);
378
+
379
+ // Generate service buttons in pairs
380
+ for (let i = 0; i < pageServices.length; i += rowSize) {
381
+ const row = [];
382
+ for (let j = 0; j < rowSize && i + j < pageServices.length; j++) {
383
+ const [serviceId, serviceData] = pageServices[i + j];
384
+ row.push(
385
+ Markup.button.callback(
386
+ `${serviceData.icon} ${serviceData.label_en}`,
387
+ `service_${serviceId}`
388
+ )
389
+ );
390
+ }
391
+ buttons.push(row);
392
+ }
393
+
394
+ buttons.push([Markup.button.callback('🔙 Back to Services', 'browse_services')]);
395
+
396
+ try {
397
+ await ctx.reply(getServicesPaginationInfo(currentPage), {
398
+ parse_mode: 'HTML',
399
+ reply_markup: Markup.inlineKeyboard(buttons).reply_markup
400
+ });
401
+ } catch (replyError: any) {
402
+ logger.error(`Error replying with product search results: ${replyError.message}`);
403
+ }
404
+
405
+ } else if (userState.waitingForCountrySearch) {
406
+ const { serviceId, sortBy, page } = userState.waitingForCountrySearch;
407
+ const allCountries = Object.entries(countryData); // Use all countries for search
408
+
409
+ const queryResults = allCountries.filter(([countryShortId, countryInfo]) => {
410
+ const searchTarget = `${countryInfo.label.toLowerCase()} ${countryInfo.flag.toLowerCase()} ${countryInfo.code.toLowerCase()} ${countryShortId.toLowerCase()}`;
411
+ return searchTarget.includes(query);
412
+ });
413
+
414
+ // Get prices for the selected service to filter countries by availability and get min/max prices
415
+ let productPricesForService: any = {};
416
+ try {
417
+ const fullProductPrices = await virtualNumberService.getProductPrices(serviceId);
418
+ if (fullProductPrices && fullProductPrices[serviceId]) {
419
+ productPricesForService = fullProductPrices[serviceId];
420
+ }
421
+ } catch (error: any) {
422
+ logger.error(`Error fetching prices for service ${serviceId} during country search: ${error.message}`);
423
+ try {
424
+ await ctx.reply(getServiceErrorMessage(serviceId), {
425
+ parse_mode: 'HTML',
426
+ reply_markup: Markup.inlineKeyboard([Markup.button.callback('🔙 Back to Services', 'browse_services')]).reply_markup
427
+ });
428
+ } catch (replyError: any) {
429
+ logger.error(`Error replying with service error message during country search: ${replyError.message}`);
430
+ }
431
+ return;
432
+ }
433
+
434
+ const searchResults = queryResults.filter(([countryShortId, _]) => {
435
+ const countryOperators = productPricesForService[countryShortId];
436
+ // Only include countries that have available numbers for the current service
437
+ return countryOperators && Object.values(countryOperators).some((op: any) => op.count > 0);
438
+ }).map(([countryId, countryInfo]) => {
439
+ const operators = productPricesForService[countryId];
440
+ const prices = Object.values(operators)
441
+ .filter((op: any) => op.count > 0)
442
+ .map((op: any) => calculatePriceWithProfit(ctx, op.cost, 'RUB'));
443
+
444
+ const minPrice = prices.length > 0 ? parseFloat(Math.min(...prices).toFixed(2)) : 0;
445
+ const maxPrice = prices.length > 0 ? parseFloat(Math.max(...prices).toFixed(2)) : 0;
446
+
447
+ return { countryId, countryInfo, minPrice, maxPrice };
448
+ });
449
+
450
+ if (searchResults.length === 0) {
451
+ try {
452
+ await ctx.reply(getNoCountrySearchResultsMessage(), {
453
+ reply_markup: Markup.inlineKeyboard([Markup.button.callback('↩️ Cancel Search', `cancel_country_search_${serviceId}_${sortBy || 'default'}_${page}`)]).reply_markup
454
+ });
455
+ } catch (replyError: any) {
456
+ logger.error(`Error replying with no country search results message: ${replyError.message}`);
457
+ }
458
+ return;
459
+ }
460
+
461
+ const buttons = [];
462
+ const rowSize = 2;
463
+ const countriesPerPage = 10;
464
+
465
+ const currentPage = 0;
466
+
467
+ const startIndex = currentPage * countriesPerPage;
468
+ const endIndex = Math.min(startIndex + countriesPerPage, searchResults.length);
469
+ const pageCountries = searchResults.slice(startIndex, endIndex);
470
+
471
+ for (let i = 0; i < pageCountries.length; i += rowSize) {
472
+ const row = [];
473
+ for (let j = 0; j < rowSize && i + j < pageCountries.length; j++) {
474
+ const { countryId, countryInfo, minPrice, maxPrice } = pageCountries[i + j];
475
+
476
+ const priceText = minPrice === maxPrice
477
+ ? `${minPrice}$`
478
+ : `${minPrice}$-${maxPrice}$`;
479
+
480
+ row.push(
481
+ Markup.button.callback(
482
+ `${countryInfo.flag} ${countryInfo.label} (${priceText})`,
483
+ `country_${serviceId}_${countryId}`
484
+ )
485
+ );
486
+ }
487
+ buttons.push(row);
488
+ }
489
+
490
+ buttons.push([Markup.button.callback('↩️ Back to Countries', `cancel_country_search_${serviceId}_${sortBy || 'default'}_${page}`)]);
491
+
492
+ try {
493
+ await ctx.reply(getCountriesListMessage(serviceId), {
494
+ parse_mode: 'HTML',
495
+ reply_markup: Markup.inlineKeyboard(buttons).reply_markup
496
+ });
497
+ } catch (replyError: any) {
498
+ logger.error(`Error replying with country search results: ${replyError.message}`);
499
+ }
500
+ }
501
+ } catch (handlerError: any) {
502
+ logger.error(`Unhandled error in text handler: ${handlerError.message}`);
503
+ try {
504
+ await ctx.reply('⚠️ An unexpected error occurred. Please try again or go back to the main menu.', {
505
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
506
+ });
507
+ } catch (finalReplyError: any) {
508
+ logger.error(`Error sending final error message: ${finalReplyError.message}`);
509
+ }
510
+ }
511
+ });
512
+
513
+ // Register service selection handlers
514
+ bot.action(/^service_(.+)$/, async (ctx: BotContext) => {
515
+ await ctx.answerCbQuery();
516
+ const match = ctx.match as RegExpMatchArray;
517
+ const service = match[1];
518
+ await handleServiceSelection(ctx, service);
519
+ });
520
+
521
+ // Register country selection handler
522
+ bot.action(/^country_(.+)_(.+)$/, async (ctx: BotContext) => {
523
+ await ctx.answerCbQuery();
524
+ await handleCountrySelection(ctx);
525
+ });
526
+ };
527
+
528
+ export const handleBrowseServices = async (ctx: BotContext, sortBy?: 'az' | 'za') => {
529
+ // 🔒 Authentication check
530
+ const telegramId = ctx.from?.id;
531
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
532
+ try {
533
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
534
+ } catch (editError: any) {
535
+ if (editError.message && editError.message.includes('message is not modified')) {
536
+ logger.info(`Auth required message not modified. No action needed.`);
537
+ } else {
538
+ logger.error(`Error editing auth required message: ${editError.message}`);
539
+ }
540
+ }
541
+ return;
542
+ }
543
+
544
+ try {
545
+ await ctx.editMessageText(
546
+ getServicesPaginationInfo(0),
547
+ {
548
+ parse_mode: 'HTML',
549
+ reply_markup: getServicesKeyboard(0, sortBy).reply_markup
550
+ }
551
+ );
552
+ } catch (editError: any) {
553
+ if (editError.message && editError.message.includes('message is not modified')) {
554
+ logger.info(`Services menu message not modified. No action needed.`);
555
+ } else {
556
+ logger.error(`Error editing services menu message: ${editError.message}`);
557
+ }
558
+ }
559
+ };
560
+
561
+ export const handleServicesPagination = async (ctx: BotContext, page: number, sortBy?: 'az' | 'za') => {
562
+ // 🔒 Authentication check
563
+ const telegramId = ctx.from?.id;
564
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
565
+ try {
566
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
567
+ } catch (editError: any) {
568
+ if (editError.message && editError.message.includes('message is not modified')) {
569
+ logger.info(`Auth required message not modified. No action needed.`);
570
+ } else {
571
+ logger.error(`Error editing auth required message: ${editError.message}`);
572
+ }
573
+ }
574
+ return; // Explicitly return nothing after editing
575
+ }
576
+
577
+ try {
578
+ // Edit the existing message instead of sending a new one
579
+ try {
580
+ await ctx.editMessageText(
581
+ getServicesPaginationInfo(page),
582
+ {
583
+ parse_mode: 'HTML',
584
+ reply_markup: getServicesKeyboard(page, sortBy).reply_markup // Pass sortBy here
585
+ }
586
+ );
587
+ } catch (editError: any) {
588
+ if (editError.message && editError.message.includes('message is not modified')) {
589
+ logger.info(`Services page message not modified for page ${page}. No action needed.`);
590
+ } else {
591
+ throw editError; // Re-throw other errors to be caught by the outer catch
592
+ }
593
+ }
594
+ } catch (error: any) {
595
+ logger.error(`Error updating services page ${page}: ${error.message}`);
596
+ try {
597
+ await ctx.editMessageText(
598
+ '⚠️ Error updating services list. Please try again.',
599
+ {
600
+ parse_mode: 'HTML',
601
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
602
+ }
603
+ );
604
+ } catch (editError: any) {
605
+ if (editError.message && editError.message.includes('message is not modified')) {
606
+ logger.info(`Services page message not modified for page ${page}. No action needed.`);
607
+ } else {
608
+ logger.error(`Error editing message in services pagination error handler for page ${page}: ${editError.message}`);
609
+ }
610
+ }
611
+ }
612
+ };
613
+
614
+ export const handleServiceSelection = async (ctx: BotContext, service: string, sortBy?: 'price_asc', page: number = 0) => {
615
+ // 🔒 Authentication check
616
+ const telegramId = ctx.from?.id;
617
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
618
+ try {
619
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
620
+ } catch (editError: any) {
621
+ if (editError.message && editError.message.includes('message is not modified')) {
622
+ logger.info(`Auth required message not modified (handleServiceSelection). No action needed.`);
623
+ } else {
624
+ logger.error(`Error editing auth required message (handleServiceSelection): ${editError.message}`);
625
+ }
626
+ }
627
+ return; // Explicitly return nothing after editing
628
+ }
629
+
630
+ try {
631
+ // Get all prices for the selected service
632
+ const productPrices = await virtualNumberService.getProductPrices(service);
633
+ const servicePrices = productPrices[service];
634
+
635
+ if (!servicePrices) {
636
+ try {
637
+ await ctx.editMessageText(
638
+ getServiceErrorMessage(service),
639
+ {
640
+ parse_mode: 'HTML',
641
+ reply_markup: Markup.inlineKeyboard([
642
+ [Markup.button.callback('🔙 Back to Services', 'browse_services')],
643
+ ]).reply_markup
644
+ }
645
+ );
646
+ } catch (editError: any) {
647
+ if (editError.response && editError.response.error_code === 400 && editError.response.description.includes('message is not modified')) {
648
+ logger.info(`Error message already displayed for service ${service}. No action needed.`);
649
+ } else {
650
+ logger.error(`Error editing message when service prices not found for service ${service}: ${editError.message}`);
651
+ }
652
+ }
653
+ return; // Return after attempting to edit the message
654
+ }
655
+
656
+ // Create buttons for countries with available numbers
657
+ const buttons = [];
658
+ const rowSize = 2; // 2 buttons per row
659
+ const countriesPerPage = 20; // 10 countries per page
660
+ let countries = Object.entries(servicePrices)
661
+ .filter(([_, operators]) => {
662
+ // Check if any operator has available numbers
663
+ return Object.values(operators).some(op => op.count > 0);
664
+ })
665
+ .map(([countryId, operators]) => {
666
+ // Get all prices with profit applied
667
+ const prices = Object.values(operators)
668
+ .filter(op => op.count > 0)
669
+ .map(op => calculatePriceWithProfit(ctx, op.cost, 'RUB'));
670
+
671
+ // Calculate min and max prices
672
+ const minPrice = Math.min(...prices);
673
+ const maxPrice = Math.max(...prices);
674
+
675
+ // Round prices to 2 decimal places
676
+ const roundedMinPrice = parseFloat(minPrice.toFixed(2));
677
+ const roundedMaxPrice = parseFloat(maxPrice.toFixed(2));
678
+
679
+ return { countryId, minPrice: roundedMinPrice, maxPrice: roundedMaxPrice };
680
+ });
681
+
682
+ // Apply sorting based on sortBy parameter
683
+ if (sortBy === 'price_asc') {
684
+ countries.sort((a, b) => a.minPrice - b.minPrice);
685
+ } else {
686
+ // Default sort by countryId
687
+ countries.sort((a, b) => a.countryId.localeCompare(b.countryId));
688
+ }
689
+
690
+ const totalPages = Math.ceil(countries.length / countriesPerPage);
691
+
692
+ // Get countries for current page
693
+ const startIndex = page * countriesPerPage;
694
+ const endIndex = Math.min(startIndex + countriesPerPage, countries.length);
695
+ const pageCountries = countries.slice(startIndex, endIndex);
696
+
697
+ // Add sort button
698
+ buttons.push([Markup.button.callback('Price ⬇️ (Cheapest)', `sort_countries_price_asc_${service}_${page}`)]);
699
+
700
+ // Add search country button
701
+ buttons.push([Markup.button.callback('🔍 Search Country', `search_country_${service}_${sortBy || 'default'}_${page}`)]);
702
+
703
+ // Generate country buttons in pairs
704
+ for (let i = 0; i < pageCountries.length; i += rowSize) {
705
+ const row = [];
706
+ for (let j = 0; j < rowSize && i + j < pageCountries.length; j++) {
707
+ const { countryId, minPrice, maxPrice } = pageCountries[i + j];
708
+ const countryInfo = countryData[countryId.toLowerCase()];
709
+ if (countryInfo) {
710
+ const priceText = minPrice === maxPrice
711
+ ? `${minPrice}$`
712
+ : `${minPrice}$-${maxPrice}$`;
713
+
714
+ row.push(
715
+ Markup.button.callback(
716
+ `${countryInfo.flag} ${countryInfo.label} (${priceText})`,
717
+ `country_${service}_${countryId}`
718
+ )
719
+ );
720
+ }
721
+ }
722
+ if (row.length > 0) {
723
+ buttons.push(row);
724
+ }
725
+ }
726
+
727
+ // Add pagination buttons
728
+ const paginationRow = [];
729
+ if (page > 0) {
730
+ paginationRow.push(
731
+ Markup.button.callback(
732
+ '⬅️ Previous',
733
+ `country_page_${service}_${page - 1}`
734
+ )
735
+ );
736
+ }
737
+
738
+ paginationRow.push(
739
+ Markup.button.callback(
740
+ `📄 ${page + 1}/${totalPages}`,
741
+ 'noop'
742
+ )
743
+ );
744
+
745
+ if (page < totalPages - 1) {
746
+ paginationRow.push(
747
+ Markup.button.callback(
748
+ 'Next ➡️',
749
+ `country_page_${service}_${page + 1}`
750
+ )
751
+ );
752
+ }
753
+
754
+ if (paginationRow.length > 0) {
755
+ buttons.push(paginationRow);
756
+ }
757
+
758
+ // Add back button
759
+ buttons.push([
760
+ Markup.button.callback('🔙 Back to Services', 'browse_services')
761
+ ]);
762
+
763
+ // Edit the existing message instead of sending a new one
764
+ try {
765
+ await ctx.editMessageText(
766
+ getCountriesListMessage(service),
767
+ {
768
+ parse_mode: 'HTML',
769
+ reply_markup: Markup.inlineKeyboard(buttons).reply_markup
770
+ }
771
+ );
772
+ return; // Explicitly return after successful edit
773
+ } catch (editError: any) {
774
+ if (editError.message && editError.message.includes('message is not modified')) {
775
+ logger.info(`Message not modified for service ${service} on page ${page}. No action needed.`);
776
+ } else {
777
+ logger.error(`Error editing message for service ${service} on page ${page}: ${editError.message}`);
778
+ }
779
+ return; // Explicitly return after handling edit error
780
+ }
781
+ } catch (error: any) {
782
+ logger.error(`Error fetching countries for ${service}: ${error.message}`);
783
+ try {
784
+ await ctx.editMessageText(
785
+ getServiceErrorMessage(service),
786
+ {
787
+ parse_mode: 'HTML',
788
+ reply_markup: Markup.inlineKeyboard([
789
+ [Markup.button.callback('🔙 Back to Services', 'browse_services')],
790
+ ]).reply_markup
791
+ }
792
+ );
793
+ return; // Explicitly return after displaying service error message
794
+ } catch (editError: any) {
795
+ if (editError.message && editError.message.includes('message is not modified')) {
796
+ logger.info(`Error message already displayed for service ${service}. No action needed.`);
797
+ } else {
798
+ logger.error(`Error editing message in error handler for service ${service}: ${editError.message}`);
799
+ }
800
+ return; // Explicitly return after handling error message edit error
801
+ }
802
+ }
803
+ };
804
+
805
+ export const handleCountrySelection = async (ctx: BotContext) => {
806
+ const match = ctx.match as RegExpMatchArray;
807
+ const service = match[1];
808
+ const countryId = match[2];
809
+
810
+ // Authentication check
811
+ const telegramId = ctx.from?.id;
812
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
813
+ await ctx.editMessageText(getAuthRequiredMessage(), { reply_markup: getMainMenuKeyboard().reply_markup });
814
+ return;
815
+ }
816
+
817
+ try {
818
+ const prices = await virtualNumberService.getPrices(service, countryId);
819
+
820
+ if (!prices || !prices[countryId] || !prices[countryId][service] ||
821
+ Object.keys(prices[countryId][service]).length === 0) {
822
+ await ctx.editMessageText(
823
+ getNoPricesMessage(service, countryId),
824
+ {
825
+ parse_mode: 'HTML',
826
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
827
+ }
828
+ );
829
+ return;
830
+ }
831
+
832
+ // Apply profit to prices before displaying
833
+ const pricesWithProfit = Object.entries(prices[countryId][service]).reduce((acc: any, [operator, operatorData]) => {
834
+ // operatorData contains cost and count properties
835
+ if (operatorData && typeof operatorData === 'object' && 'cost' in operatorData) {
836
+ acc[operator] = {
837
+ ...operatorData,
838
+ cost: calculatePriceWithProfit(ctx, Number(operatorData.cost), 'RUB')
839
+ };
840
+ }
841
+ return acc;
842
+ }, {});
843
+
844
+ try {
845
+ await ctx.editMessageText(
846
+ getPricesListMessage(service, countryId, pricesWithProfit, ctx),
847
+ {
848
+ parse_mode: 'HTML',
849
+ reply_markup: getServicePricesKeyboard(pricesWithProfit, service, countryId, ctx).reply_markup
850
+ }
851
+ );
852
+ } catch (editError: any) {
853
+ if (editError.message && editError.message.includes('message is not modified')) {
854
+ logger.info(`Prices list message not modified for service ${service} in country ${countryId}. No action needed.`);
855
+ } else {
856
+ logger.error(`Error editing prices list message for service ${service} in country ${countryId}: ${editError.message}`);
857
+ }
858
+ }
859
+
860
+ } catch (error: any) {
861
+ logger.error(`Error fetching prices for ${service} in ${countryId}: ${error.message}`);
862
+ try {
863
+ await ctx.editMessageText(
864
+ getPricesErrorMessage(service, countryId),
865
+ {
866
+ parse_mode: 'HTML',
867
+ reply_markup: getLoggedInMenuKeyboard().reply_markup
868
+ }
869
+ );
870
+ } catch (editError: any) {
871
+ if (editError.message && editError.message.includes('message is not modified')) {
872
+ logger.info(`Error message already displayed for service ${service} in country ${countryId}. No action needed.`);
873
+ } else {
874
+ logger.error(`Error editing message in country selection error handler for service ${service} in country ${countryId}: ${editError.message}`);
875
+ }
876
+ }
877
+ }
878
+ };
src/bots/index.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { getBotIdFromToken, isValidBotToken } from "../utils/botUtils";
7
+
8
+ const logger = createLogger('BotManager');
9
+
10
+ // Utility function to update bot status in database
11
+ const updateBotStatus = async (botId: string, isActive: boolean, error?: string) => {
12
+ try {
13
+ await supabase
14
+ .from('bots')
15
+ .update({
16
+ is_active: isActive,
17
+ last_activity: new Date().toISOString(),
18
+ state: {
19
+ status: isActive ? 'running' : 'error',
20
+ startedAt: isActive ? new Date().toISOString() : undefined,
21
+ error: error
22
+ }
23
+ })
24
+ .eq('id', botId);
25
+ } catch (error: any) {
26
+ logger.error(`Error updating bot status: ${error.message}`);
27
+ }
28
+ };
29
+
30
+ export const handleAddTelegrafBot = async (botId: string) => {
31
+ if (!botId) {
32
+ return { status: 400, data: { error: "Bot ID is required" } };
33
+ }
34
+
35
+ try {
36
+ logger.info(`Fetching bot data for bot ID: ${botId}`);
37
+ // Fetch bot data from database
38
+ const { data: botData, error } = await supabase
39
+ .from('bots')
40
+ .select('*')
41
+ .eq('id', botId)
42
+ .single();
43
+
44
+ if (error) {
45
+ logger.error(`Error fetching bot data: ${error.message}`);
46
+ return { status: 404, data: { error: `Bot not found: ${error.message}` } };
47
+ }
48
+
49
+ if (!botData || !botData.bot_token) {
50
+ return { status: 404, data: { error: "Bot token not found in database" } };
51
+ }
52
+
53
+ // Validate bot token format
54
+ if (!isValidBotToken(botData.bot_token)) {
55
+ logger.error(`Invalid bot token format for bot ${botId}`);
56
+ return { status: 400, data: { error: "Invalid bot token format" } };
57
+ }
58
+
59
+ // Check if bot is already running
60
+ if (telegrafBots.has(botData.bot_token)) {
61
+ return { status: 200, data: { error: "Bot is already running" } };
62
+ }
63
+
64
+ try {
65
+ // Initialize the bot with settings from database
66
+ const result = await initializeBot(botData.bot_token, botData);
67
+
68
+ if (result.success && result.bot) {
69
+ // Store the bot instance in the map
70
+ telegrafBots.set(botData.bot_token, result.bot);
71
+
72
+ try {
73
+ // Update bot status to active
74
+ await updateBotStatus(botId, true);
75
+
76
+ logger.info(`Bot ${botData.name} (${botId}) launched successfully`);
77
+
78
+ await result.bot.launch();
79
+
80
+ // Setup payment webhook handlers
81
+ // setupPaymentWebhookHandlers(result.bot);
82
+
83
+ return { status: 200, data: {
84
+ message: result.message,
85
+ bot: {
86
+ id: botData.id,
87
+ name: botData.name,
88
+ is_active: true
89
+ }
90
+ }};
91
+
92
+ } catch (error: any) {
93
+ logger.error(`Error launching bot: ${error.message}`);
94
+ await updateBotStatus(botId, false, error.message);
95
+ return { status: 500, data: { error: `Error launching bot: ${error.message}` } };
96
+ }
97
+ } else {
98
+ logger.error(`Error initializing bot: ${result.message}`);
99
+ await updateBotStatus(botId, false, result.message);
100
+ return { status: 500, data: { error: result.message } };
101
+ }
102
+ } catch (error: any) {
103
+ logger.error(`Bot initialization error: ${error.message}`);
104
+ await updateBotStatus(botId, false, error.message);
105
+ return { status: 500, data: { error: `Bot initialization error: ${error.message}` } };
106
+ }
107
+ } catch (error: any) {
108
+ logger.error(`Unexpected error: ${error.message}`);
109
+ await updateBotStatus(botId, false, error.message);
110
+ return { status: 500, data: { error: `Unexpected error: ${error.message}` } };
111
+ }
112
+ };
src/bots/middleware/groupCheckMiddleware.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import { createLogger } from '../../utils/logger';
3
+ import { messageManager } from '../utils/messageManager';
4
+
5
+ const logger = createLogger('GroupCheckMiddleware');
6
+
7
+ export const groupCheckMiddleware = async (ctx: BotContext, next: () => Promise<void>) => {
8
+ try {
9
+ // Skip check for non-message updates
10
+ if (!ctx.message || !ctx.from) {
11
+ return next();
12
+ }
13
+
14
+ const botData = ctx.botData;
15
+ if (!botData?.join_group_required || !botData?.group_channel_username) {
16
+ return next();
17
+ }
18
+
19
+ // Skip check for admin commands
20
+ if ('text' in ctx.message && ctx.message.text?.startsWith('/')) {
21
+ return next();
22
+ }
23
+
24
+ try {
25
+ // Check if user is a member of the required group
26
+ const chatMember = await ctx.telegram.getChatMember(
27
+ botData.group_channel_username,
28
+ ctx.from.id
29
+ );
30
+
31
+ if (['member', 'administrator', 'creator'].includes(chatMember.status)) {
32
+ return next();
33
+ }
34
+
35
+ // User is not a member of the required group
36
+ const message = messageManager.getMessage('join_group_required');
37
+
38
+ await ctx.reply(message, {
39
+ parse_mode: 'HTML',
40
+ link_preview_options: { is_disabled: true }
41
+ });
42
+
43
+ } catch (error: any) {
44
+ logger.error(`Error checking group membership: ${error.message}`);
45
+ // If there's an error checking membership, allow the user to proceed
46
+ return next();
47
+ }
48
+ } catch (error: any) {
49
+ logger.error(`Error in group check middleware: ${error.message}`);
50
+ return next();
51
+ }
52
+ };
src/bots/services/AdminService.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import { createLogger } from '../../utils/logger';
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(ctx: BotContext) {
11
+ this.adminContact = ctx.botData?.admin_contact || '';
12
+ }
13
+
14
+ public static getInstance(ctx: BotContext): AdminService {
15
+ if (!AdminService.instance) {
16
+ AdminService.instance = new AdminService(ctx);
17
+ }
18
+ return AdminService.instance;
19
+ }
20
+
21
+ async getAdminContact(): Promise<string> {
22
+ return this.adminContact;
23
+ }
24
+ }
src/bots/services/BalanceUpdateService.ts ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { supabase } from '../../db/supabase';
2
+ import { createLogger } from '../../utils/logger';
3
+ import { BotContext } from '../types/botTypes';
4
+ import { messageManager } from '../utils/messageManager';
5
+ import { initializeBot, stopBot } from '../botManager';
6
+
7
+ const logger = createLogger('BalanceUpdateService');
8
+
9
+ export class BalanceUpdateService {
10
+ private static instance: BalanceUpdateService;
11
+ private bot: any;
12
+ private subscription: any;
13
+ private botsSubscription: any;
14
+
15
+ private constructor() {
16
+ // Delay the initial subscription setup to ensure bot is ready
17
+ setTimeout(() => {
18
+ //this.setupRealtimeSubscription();
19
+ //this.setupBotsSubscription();
20
+ }, 1000);
21
+ }
22
+
23
+ public static getInstance(): BalanceUpdateService {
24
+ if (!BalanceUpdateService.instance) {
25
+ BalanceUpdateService.instance = new BalanceUpdateService();
26
+ }
27
+ return BalanceUpdateService.instance;
28
+ }
29
+
30
+ public setBot(bot: any) {
31
+ this.bot = bot;
32
+ logger.info('Bot instance set in BalanceUpdateService');
33
+ // Setup subscription after bot is set
34
+ this.setupRealtimeSubscription();
35
+ }
36
+
37
+ private async setupRealtimeSubscription() {
38
+ try {
39
+ logger.info('Starting realtime subscription setup...');
40
+
41
+ // Unsubscribe from any existing subscription
42
+ if (this.subscription) {
43
+ await this.subscription.unsubscribe();
44
+ logger.info('Unsubscribed from previous subscription');
45
+ }
46
+
47
+ // Create a new channel
48
+ const channel = supabase.channel('balance_changes', {
49
+ config: {
50
+ broadcast: { self: true }
51
+ }
52
+ });
53
+
54
+ // Subscribe to changes
55
+ channel
56
+ .on(
57
+ 'postgres_changes',
58
+ {
59
+ event: 'UPDATE',
60
+ schema: 'public',
61
+ table: 'users_bot_telegram'
62
+ },
63
+ async (payload: any) => {
64
+ logger.info('Received database change:', JSON.stringify(payload, null, 2));
65
+
66
+ // Check if balance was actually changed
67
+ if (payload.old.balance !== payload.new.balance) {
68
+ logger.info('Balance change detected, processing update...');
69
+ try {
70
+ await this.handleBalanceUpdate(payload);
71
+ } catch (error) {
72
+ logger.error('Error handling balance update:', error);
73
+ }
74
+ } else {
75
+ logger.info('No balance change detected in update');
76
+ }
77
+ }
78
+ )
79
+ .subscribe((status: string) => {
80
+ logger.info('Subscription status changed:', status);
81
+ if (status === 'SUBSCRIBED') {
82
+ logger.info('Successfully subscribed to balance changes');
83
+ this.subscription = channel;
84
+ } else if (status === 'CLOSED') {
85
+ logger.error('Subscription closed unexpectedly');
86
+ setTimeout(() => this.setupRealtimeSubscription(), 5000);
87
+ } else if (status === 'CHANNEL_ERROR') {
88
+ logger.error('Channel error occurred');
89
+ setTimeout(() => this.setupRealtimeSubscription(), 5000);
90
+ }
91
+ });
92
+
93
+ // Add connection status listener
94
+ supabase
95
+ .channel('system')
96
+ .on('system', { event: '*' }, (payload) => {
97
+ logger.info('System event:', payload);
98
+ })
99
+ .subscribe();
100
+
101
+ logger.info('Balance update subscription setup completed');
102
+ } catch (error) {
103
+ logger.error('Error setting up realtime subscription:', error);
104
+ setTimeout(() => this.setupRealtimeSubscription(), 5000);
105
+ }
106
+ }
107
+
108
+ private async setupBotsSubscription() {
109
+ try {
110
+ logger.info('Starting bots subscription setup...');
111
+
112
+ // Unsubscribe from any existing subscription
113
+ if (this.botsSubscription) {
114
+ await this.botsSubscription.unsubscribe();
115
+ logger.info('Unsubscribed from previous bots subscription');
116
+ }
117
+
118
+ // Create a new channel for bots
119
+ const channel = supabase.channel('bots_changes', {
120
+ config: {
121
+ broadcast: { self: true }
122
+ }
123
+ });
124
+
125
+ // Subscribe to changes in bots table
126
+ channel
127
+ .on(
128
+ 'postgres_changes',
129
+ {
130
+ event: '*', // Listen to all events (INSERT, UPDATE, DELETE)
131
+ schema: 'public',
132
+ table: 'bots'
133
+ },
134
+ async (payload: any) => {
135
+ logger.info('Received bots table change:', JSON.stringify(payload, null, 2));
136
+
137
+ try {
138
+ const botId = payload.new?.id || payload.old?.id;
139
+ if (!botId) {
140
+ logger.error('No bot ID found in payload');
141
+ return;
142
+ }
143
+
144
+ // Stop the bot first
145
+ await stopBot(botId);
146
+ logger.info(`Stopped bot ${botId}`);
147
+
148
+ // If it's an UPDATE or INSERT, restart the bot with new data
149
+ if (payload.eventType === 'UPDATE' || payload.eventType === 'INSERT') {
150
+ const { data: botData, error } = await supabase
151
+ .from('bots')
152
+ .select('*')
153
+ .eq('id', botId)
154
+ .single();
155
+
156
+ if (error) {
157
+ logger.error(`Error fetching bot data: ${error.message}`);
158
+ return;
159
+ }
160
+
161
+ if (botData) {
162
+ const result = await initializeBot(botData.bot_token, botData);
163
+ if (result.success) {
164
+ logger.info(`Bot ${botId} restarted successfully`);
165
+ } else {
166
+ logger.error(`Failed to restart bot ${botId}: ${result.message}`);
167
+ }
168
+ }
169
+ }
170
+ } catch (error) {
171
+ logger.error('Error handling bot update:', error);
172
+ }
173
+ }
174
+ )
175
+ .subscribe((status: string) => {
176
+ logger.info('Bots subscription status changed:', status);
177
+ if (status === 'SUBSCRIBED') {
178
+ logger.info('Successfully subscribed to bots changes');
179
+ this.botsSubscription = channel;
180
+ } else if (status === 'CLOSED') {
181
+ logger.error('Bots subscription closed unexpectedly');
182
+ setTimeout(() => this.setupBotsSubscription(), 5000);
183
+ } else if (status === 'CHANNEL_ERROR') {
184
+ logger.error('Bots channel error occurred');
185
+ setTimeout(() => this.setupBotsSubscription(), 5000);
186
+ }
187
+ });
188
+
189
+ logger.info('Bots subscription setup completed');
190
+ } catch (error) {
191
+ logger.error('Error setting up bots subscription:', error);
192
+ setTimeout(() => this.setupBotsSubscription(), 5000);
193
+ }
194
+ }
195
+
196
+ private async handleBalanceUpdate(payload: any) {
197
+ logger.info('Processing balance update payload:', JSON.stringify(payload, null, 2));
198
+
199
+ if (!this.bot) {
200
+ logger.error('Bot instance not set in BalanceUpdateService');
201
+ return;
202
+ }
203
+
204
+ const { old: oldRecord, new: newRecord } = payload;
205
+
206
+ if (!oldRecord || !newRecord) {
207
+ logger.error('Invalid payload format:', payload);
208
+ return;
209
+ }
210
+
211
+ const telegramId = newRecord.telegram_id;
212
+ const oldBalance = Number(oldRecord.balance) || 0;
213
+ const newBalance = Number(newRecord.balance) || 0;
214
+
215
+ logger.info(`Balance change detected for user ${telegramId}: ${oldBalance} -> ${newBalance}`);
216
+
217
+ // Only send notification if balance actually changed
218
+ if (oldBalance !== newBalance) {
219
+ try {
220
+ const balanceChange = newBalance - oldBalance;
221
+ const changeType = balanceChange > 0 ? 'increase' : 'decrease';
222
+
223
+ const message = messageManager.getMessage('balance_update_notification')
224
+ .replace('{old_balance}', oldBalance.toFixed(2))
225
+ .replace('{new_balance}', newBalance.toFixed(2))
226
+ .replace('{change_type}', changeType)
227
+ .replace('{change_amount}', Math.abs(balanceChange).toFixed(2));
228
+
229
+ logger.info(`Sending balance update message to user ${telegramId}:`, message);
230
+
231
+ await this.bot.telegram.sendMessage(telegramId, message, { parse_mode: 'HTML' });
232
+ logger.info(`Balance update notification sent to user ${telegramId}`);
233
+ } catch (error) {
234
+ logger.error(`Failed to send balance update notification to user ${telegramId}:`, error);
235
+ }
236
+ } else {
237
+ logger.info(`No balance change detected for user ${telegramId}`);
238
+ }
239
+ }
240
+
241
+ // Method to manually trigger a balance update notification
242
+ public async sendBalanceUpdateNotification(telegramId: number, oldBalance: number, newBalance: number) {
243
+ try {
244
+ const balanceChange = newBalance - oldBalance;
245
+ const changeType = balanceChange > 0 ? 'increase' : 'decrease';
246
+
247
+ const message = messageManager.getMessage('balance_update_notification')
248
+ .replace('{old_balance}', oldBalance.toFixed(2))
249
+ .replace('{new_balance}', newBalance.toFixed(2))
250
+ .replace('{change_type}', changeType)
251
+ .replace('{change_amount}', Math.abs(balanceChange).toFixed(2));
252
+
253
+ await this.bot.telegram.sendMessage(telegramId, message, { parse_mode: 'HTML' });
254
+ logger.info(`Manual balance update notification sent to user ${telegramId}`);
255
+ } catch (error) {
256
+ logger.error(`Failed to send manual balance update notification to user ${telegramId}:`, error);
257
+ }
258
+ }
259
+
260
+ // Method to check subscription status
261
+ public getSubscriptionStatus(): string {
262
+ return this.subscription?.state || 'NOT_INITIALIZED';
263
+ }
264
+ }
265
+
266
+ // export const balanceUpdateService = BalanceUpdateService.getInstance();
src/bots/services/CryptoService.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import { createLogger } from '../../utils/logger';
3
+ import { supabase } from '../../db/supabase';
4
+
5
+ const logger = createLogger('CryptoService');
6
+
7
+ export class CryptoService {
8
+ private static instance: CryptoService;
9
+ private readonly cryptoAddress: string;
10
+
11
+ private constructor(ctx: BotContext) {
12
+ this.cryptoAddress = ctx.botData?.crypto_wallet_address || '';
13
+ }
14
+
15
+ public static getInstance(ctx: BotContext): CryptoService {
16
+ if (!CryptoService.instance) {
17
+ CryptoService.instance = new CryptoService(ctx);
18
+ }
19
+ return CryptoService.instance;
20
+ }
21
+
22
+ async getPaymentAddress(userId: string, amount: number): Promise<string> {
23
+ try {
24
+ // Create payment record in database
25
+ const { data: payment, error } = await supabase
26
+ .from('payments')
27
+ .insert({
28
+ user_id: userId,
29
+ amount: amount,
30
+ status: 'PENDING',
31
+ payment_method: 'CRYPTO'
32
+ })
33
+ .select()
34
+ .single();
35
+
36
+ if (error) throw error;
37
+
38
+ return this.cryptoAddress;
39
+ } catch (error) {
40
+ logger.error('Error creating crypto payment:', error);
41
+ throw error;
42
+ }
43
+ }
44
+ }
src/bots/services/PayPalService.ts ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BotContext } from '../types/botTypes';
2
+ import axios from 'axios';
3
+ import { createLogger } from '../../utils/logger';
4
+
5
+ const logger = createLogger('PayPalService');
6
+
7
+ // PayPal API Response Types
8
+ interface PayPalLink {
9
+ href: string;
10
+ rel: string;
11
+ method: string;
12
+ }
13
+
14
+ interface PayPalOrder {
15
+ id: string;
16
+ status: string;
17
+ links: PayPalLink[];
18
+ }
19
+
20
+ interface PayPalTokenResponse {
21
+ access_token: string;
22
+ token_type: string;
23
+ expires_in: number;
24
+ }
25
+
26
+ export class PayPalService {
27
+ private static instance: PayPalService;
28
+ private readonly clientId: string;
29
+ private readonly clientSecret: string;
30
+ private readonly isProduction: boolean;
31
+
32
+ private constructor(ctx: BotContext) {
33
+ this.clientId = ctx.botData?.paypal_client_id || '';
34
+ this.clientSecret = ctx.botData?.paypal_client_secret || '';
35
+ this.isProduction = ctx.botData?.settings?.paypal_environment === 'production';
36
+ }
37
+
38
+ public static getInstance(ctx: BotContext): PayPalService {
39
+ if (!PayPalService.instance) {
40
+ PayPalService.instance = new PayPalService(ctx);
41
+ }
42
+ return PayPalService.instance;
43
+ }
44
+
45
+ async createPaymentLink(userId: string, amount: number): Promise<string> {
46
+ try {
47
+ const accessToken = await this.getAccessToken();
48
+ const order = await this.createPayPalOrder(amount, userId);
49
+
50
+ // Find the approval URL from the order links
51
+ const approvalLink = order.links.find(link => link.rel === 'approve');
52
+ if (!approvalLink) {
53
+ throw new Error('No approval URL found in PayPal order');
54
+ }
55
+
56
+ return approvalLink.href;
57
+ } catch (error: any) {
58
+ logger.error(`Error creating PayPal payment link: ${error.message}`);
59
+ throw new Error(`Failed to create payment link: ${error.message}`);
60
+ }
61
+ }
62
+
63
+ private async createPayPalOrder(amount: number, paymentId: string): Promise<PayPalOrder> {
64
+ try {
65
+ const accessToken = await this.getAccessToken();
66
+ const baseUrl = this.isProduction
67
+ ? 'https://api-m.paypal.com'
68
+ : 'https://api-m.sandbox.paypal.com';
69
+
70
+ const response = await axios.post(
71
+ `${baseUrl}/v2/checkout/orders`,
72
+ {
73
+ intent: 'CAPTURE',
74
+ purchase_units: [{
75
+ amount: {
76
+ currency_code: 'USD',
77
+ value: amount.toString()
78
+ },
79
+ custom_id: paymentId
80
+ }]
81
+ },
82
+ {
83
+ headers: {
84
+ 'Authorization': `Bearer ${accessToken}`,
85
+ 'Content-Type': 'application/json'
86
+ }
87
+ }
88
+ );
89
+
90
+ return response.data;
91
+ } catch (error: any) {
92
+ logger.error(`Error creating PayPal order: ${error.message}`);
93
+ throw new Error(`Failed to create PayPal order: ${error.message}`);
94
+ }
95
+ }
96
+
97
+ private async getAccessToken(): Promise<string> {
98
+ try {
99
+ const baseUrl = this.isProduction
100
+ ? 'https://api-m.paypal.com'
101
+ : 'https://api-m.sandbox.paypal.com';
102
+
103
+ const response = await axios.post(
104
+ `${baseUrl}/v1/oauth2/token`,
105
+ 'grant_type=client_credentials',
106
+ {
107
+ headers: {
108
+ 'Authorization': `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64')}`,
109
+ 'Content-Type': 'application/x-www-form-urlencoded'
110
+ }
111
+ }
112
+ );
113
+
114
+ const data = response.data as PayPalTokenResponse;
115
+ return data.access_token;
116
+ } catch (error: any) {
117
+ logger.error(`Error getting PayPal access token: ${error.message}`);
118
+ throw new Error(`Failed to get PayPal access token: ${error.message}`);
119
+ }
120
+ }
121
+ }
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,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ rate?: number;
16
+ }
17
+
18
+ export interface ServicePrice {
19
+ [country: string]: {
20
+ [operator: string]: PriceInfo;
21
+ };
22
+ }
23
+
24
+ export interface ProductPrices {
25
+ [service: string]: {
26
+ [country: string]: {
27
+ [operator: string]: PriceInfo;
28
+ };
29
+ };
30
+ }
31
+
32
+ export interface MaxPrice {
33
+ id: number;
34
+ product: string;
35
+ price: number;
36
+ CreatedAt: string; // date string
37
+ }
38
+
39
+ export class VirtualNumberService {
40
+ private static instance: VirtualNumberService;
41
+ private apiKey: string;
42
+ private baseUrl: string = 'https://5sim.net/v1';
43
+
44
+ private constructor() {
45
+ this.apiKey = process.env.FIVESIM_API_KEY || '';
46
+ if (!this.apiKey) {
47
+ logger.error('5sim API key not found in environment variables');
48
+ }
49
+ }
50
+
51
+ public static getInstance(): VirtualNumberService {
52
+ if (!VirtualNumberService.instance) {
53
+ VirtualNumberService.instance = new VirtualNumberService();
54
+ }
55
+ return VirtualNumberService.instance;
56
+ }
57
+
58
+ /**
59
+ * Get available countries for a specific service
60
+ */
61
+ async getAvailableCountries(service: string): Promise<Country[]> {
62
+ try {
63
+ const response = await axios.get(`${this.baseUrl}/guest/countries/${service}`, {
64
+ headers: {
65
+ 'Authorization': `Bearer ${this.apiKey}`,
66
+ 'Accept': 'application/json',
67
+ }
68
+ });
69
+
70
+ return response.data;
71
+ } catch (error: any) {
72
+ logger.error(`Error fetching countries for ${service}: ${error.message}`);
73
+ throw new Error(`Failed to fetch available countries: ${error.message}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Get all prices for a specific product
79
+ */
80
+ async getProductPrices(product: string): Promise<ProductPrices> {
81
+ try {
82
+ const response = await axios.get(`${this.baseUrl}/guest/prices?product=${product}`, {
83
+ headers: {
84
+ 'Accept': 'application/json',
85
+ }
86
+ });
87
+
88
+ return response.data;
89
+ } catch (error: any) {
90
+ logger.error(`Error fetching prices for product ${product}: ${error.message}`);
91
+ throw new Error(`Failed to fetch product prices: ${error.message}`);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get prices for a specific service and country
97
+ */
98
+ async getPrices(service: string, country: string): Promise<ServicePrice> {
99
+ try {
100
+ const response = await axios.get(`${this.baseUrl}/guest/prices?product=${service}&country=${country}`, {
101
+ headers: {
102
+ 'Authorization': `Bearer ${this.apiKey}`,
103
+ 'Accept': 'application/json',
104
+ }
105
+ });
106
+
107
+ return response.data;
108
+ } catch (error: any) {
109
+ logger.error(`Error fetching prices for ${service} in ${country}: ${error.message}`);
110
+ throw new Error(`Failed to fetch prices: ${error.message}`);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Purchase a number for a specific service and country
116
+ */
117
+ async purchaseNumber(service: string, country: string, operator: string): Promise<any> {
118
+ try {
119
+ const response = await axios.get(
120
+ `${this.baseUrl}/user/buy/activation/${country}/${operator}/${service}`,
121
+ {
122
+ headers: {
123
+ 'Authorization': `Bearer ${this.apiKey}`,
124
+ 'Accept': 'application/json',
125
+ }
126
+ }
127
+ );
128
+
129
+ return response.data;
130
+ } catch (error: any) {
131
+ logger.error(`Error purchasing number for ${service} in ${country}: ${error.message}`);
132
+ throw new Error(`Failed to purchase number: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Check for SMS received for a specific order
138
+ */
139
+ async checkSMS(orderId: string | number): Promise<any> {
140
+ try {
141
+ const response = await axios.get(
142
+ `${this.baseUrl}/user/check/${orderId}`,
143
+ {
144
+ headers: {
145
+ 'Authorization': `Bearer ${this.apiKey}`,
146
+ 'Accept': 'application/json',
147
+ }
148
+ }
149
+ );
150
+
151
+ return response.data;
152
+ } catch (error: any) {
153
+ logger.error(`Error checking SMS for order ${orderId}: ${error.message}`);
154
+ throw new Error(`Failed to check SMS: ${error.message}`);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get a list of established price limits.
160
+ */
161
+ async getMaxPrices(): Promise<MaxPrice[]> {
162
+ try {
163
+ const response = await axios.get(`${this.baseUrl}/user/max-prices`, {
164
+ headers: {
165
+ 'Authorization': `Bearer ${this.apiKey}`,
166
+ 'Accept': 'application/json',
167
+ }
168
+ });
169
+
170
+ return response.data;
171
+ } catch (error: any) {
172
+ logger.error(`Error fetching max prices: ${error.message}`);
173
+ throw new Error(`Failed to fetch max prices: ${error.message}`);
174
+ }
175
+ }
176
+ }
src/bots/services/auth.ts ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { BotContext } from '../types/botTypes';
10
+ import { getBotIdFromContext } from '../../utils/botUtils';
11
+
12
+ const logger = createLogger('AuthService');
13
+
14
+ interface UserBotTelegraf {
15
+ id?: number; // SERIAL in SQL becomes number in TypeScript
16
+ telegramId: number; // BIGINT in SQL becomes number in TypeScript
17
+ username?: string | null; // VARCHAR(255), optional
18
+ firstName?: string | null; // VARCHAR(255), matches SQL column name
19
+ lastName?: string | null; // VARCHAR(255), matches SQL column name
20
+ email: string; // VARCHAR(255) NOT NULL
21
+ passwordHash: string; // VARCHAR(255) NOT NULL, matches SQL column name
22
+ language?: string; // VARCHAR(10) DEFAULT 'en'
23
+ role?: string; // VARCHAR(10) DEFAULT 'user'
24
+ balance?: number; // DECIMAL(10, 2) DEFAULT 0
25
+ isBanned?: boolean; // BOOLEAN DEFAULT false
26
+ lastLogin?: Date | string | null; // TIMESTAMPTZ (can be Date, ISO string, or null)
27
+ botId?: string | null; // UUID (string in TypeScript)
28
+ createdAt?: Date | string; // TIMESTAMPTZ DEFAULT NOW()
29
+ updatedAt?: Date | string; // TIMESTAMPTZ DEFAULT NOW()
30
+ }
31
+
32
+ export class AuthService {
33
+ private static instance: AuthService;
34
+
35
+ private constructor() {
36
+ // Private constructor for singleton pattern
37
+ }
38
+
39
+ public static getInstance(): AuthService {
40
+ if (!AuthService.instance) {
41
+ AuthService.instance = new AuthService();
42
+ }
43
+ return AuthService.instance;
44
+ }
45
+
46
+ private generatePassword(): string {
47
+ return uuidv4().replace(/-/g, '').substring(0, 12);
48
+ }
49
+
50
+ public async hashPassword(password: string): Promise<string> {
51
+ const encoder = new TextEncoder();
52
+ const data = encoder.encode(password);
53
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
54
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
55
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
56
+ }
57
+
58
+ private generateEmail(telegramId: number, ctx: BotContext): string {
59
+ const botData = ctx.botData;
60
+ const suffix = botData?.suffix_email || 'saerosms.com';
61
+ return `user_${telegramId}@${suffix}`;
62
+ }
63
+
64
+ public async createUser(telegramId: number, name: string, ctx: BotContext): Promise<{ user: UserBotTelegraf; password: string }> {
65
+ try {
66
+ const botId = getBotIdFromContext(ctx);
67
+ if (!botId) {
68
+ throw new Error('Could not determine bot ID from context');
69
+ }
70
+
71
+ const email = this.generateEmail(telegramId, ctx);
72
+ const password = this.generatePassword();
73
+ const passwordHash = await this.hashPassword(password);
74
+
75
+ const userData: Omit<UserBotTelegraf, 'id'> = {
76
+ telegramId: telegramId,
77
+ firstName: name,
78
+ email,
79
+ passwordHash: passwordHash,
80
+ botId: botId,
81
+ balance: 0,
82
+ language: 'ar',
83
+ role: 'user',
84
+ isBanned: false
85
+ };
86
+
87
+ const user = await insertDataIntoTable('users_bot_telegram', userData);
88
+
89
+ logger.info(`User ${telegramId} created successfully for bot ${botId}`);
90
+ return { user, password };
91
+
92
+ } catch (error: any) {
93
+ logger.error(`Error creating user ${telegramId}: ${error.message}`);
94
+ throw new Error(`Failed to create user: ${error.message}`);
95
+ }
96
+ }
97
+
98
+ private loggedInUsers: Map<string, boolean> = new Map();
99
+
100
+ private generateUserKey(telegramId: number, ctx: BotContext): string {
101
+ const botId = getBotIdFromContext(ctx);
102
+ if (!botId) {
103
+ throw new Error('Could not determine bot ID from context');
104
+ }
105
+ return `${telegramId}_${botId}`;
106
+ }
107
+
108
+ public setUserLoggedIn(telegramId: number, ctx: BotContext, isLoggedIn: boolean = true): void {
109
+ const key = this.generateUserKey(telegramId, ctx);
110
+ const botId = getBotIdFromContext(ctx);
111
+ this.loggedInUsers.set(key, isLoggedIn);
112
+ logger.info(`User ${telegramId} for bot ${botId} login state set to ${isLoggedIn}`);
113
+ }
114
+
115
+ public isUserLoggedIn(telegramId: number, ctx: BotContext): boolean {
116
+ const key = this.generateUserKey(telegramId, ctx);
117
+ return this.loggedInUsers.get(key) || false;
118
+ }
119
+
120
+ public async loginUser(telegramId: number, ctx: BotContext): Promise<UserBotTelegraf | null> {
121
+ try {
122
+ const botId = getBotIdFromContext(ctx);
123
+ if (!botId) {
124
+ throw new Error('Could not determine bot ID from context');
125
+ }
126
+
127
+ const { data: user, error } = await supabase
128
+ .from('users_bot_telegram')
129
+ .select('*')
130
+ .eq('telegram_id', telegramId)
131
+ .eq('bot_id', botId)
132
+ .single();
133
+
134
+ if (error) {
135
+ if (error.code === 'PGRST116') {
136
+ return null; // User not found
137
+ }
138
+ throw error;
139
+ }
140
+
141
+ if (user) {
142
+ this.setUserLoggedIn(telegramId, ctx, true);
143
+ return user;
144
+ }
145
+
146
+ return null;
147
+ } catch (error) {
148
+ logger.error('Error in loginUser:', error);
149
+ return null;
150
+ }
151
+ }
152
+
153
+ public async getUserById(userId: string): Promise<UserBotTelegraf | null> {
154
+ try {
155
+ const { data: user, error } = await supabase
156
+ .from('users_bot_telegram')
157
+ .select('*')
158
+ .eq('id', userId)
159
+ .single();
160
+
161
+ if (error) {
162
+ if (error.code === 'PGRST116') {
163
+ return null;
164
+ }
165
+ throw error;
166
+ }
167
+
168
+ return user;
169
+ } catch (error: any) {
170
+ logger.error(`Error fetching user ${userId}: ${error.message}`);
171
+ throw new Error(`Failed to get user: ${error.message}`);
172
+ }
173
+ }
174
+
175
+ async getUserByTelegramId(telegramId: number, ctx: BotContext): Promise<UserBotTelegraf | null> {
176
+ try {
177
+ const botId = getBotIdFromContext(ctx);
178
+ if (!botId) {
179
+ throw new Error('Could not determine bot ID from context');
180
+ }
181
+
182
+ const { data: user, error } = await supabase
183
+ .from('users_bot_telegram')
184
+ .select('*')
185
+ .eq('telegram_id', telegramId)
186
+ .eq('bot_id', botId)
187
+ .single();
188
+
189
+ if (error) {
190
+ if (error.code === 'PGRST116') {
191
+ return null;
192
+ }
193
+ throw error;
194
+ }
195
+
196
+ return user;
197
+ } catch (error: any) {
198
+ logger.error(`Error getting user by Telegram ID: ${error.message}`);
199
+ return null;
200
+ }
201
+ }
202
+
203
+ async updateUserBalance(telegramId: number, ctx: BotContext, newBalance: number): Promise<boolean> {
204
+ try {
205
+ const botId = getBotIdFromContext(ctx);
206
+ if (!botId) {
207
+ throw new Error('Could not determine bot ID from context');
208
+ }
209
+
210
+ const { error } = await supabase
211
+ .from('users_bot_telegram')
212
+ .update({ balance: newBalance })
213
+ .eq('telegram_id', telegramId)
214
+ .eq('bot_id', botId);
215
+
216
+ if (error) throw error;
217
+ return true;
218
+ } catch (error: any) {
219
+ logger.error(`Error updating user balance: ${error.message}`);
220
+ return false;
221
+ }
222
+ }
223
+ }
224
+
225
+ // Singleton instance
226
+ 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/5sim_products.json ADDED
The diff for this file is too large to render. See raw diff
 
src/bots/utils/country.ts ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface CountryInfo {
2
+ label: string;
3
+ flag: string;
4
+ code: string;
5
+ }
6
+
7
+ interface CountryData {
8
+ [key: string]: CountryInfo;
9
+ }
10
+
11
+ export const countryData: CountryData = {
12
+ afghanistan: { label: "Afghanistan", flag: "🇦🇫", code: "+93" },
13
+ albania: { label: "Albania", flag: "🇦🇱", code: "+355" },
14
+ algeria: { label: "Algeria", flag: "🇩🇿", code: "+213" },
15
+ angola: { label: "Angola", flag: "🇦🇴", code: "+244" },
16
+ antiguaandbarbuda: { label: "Antigua and Barbuda", flag: "🇦🇬", code: "+1" },
17
+ argentina: { label: "Argentina", flag: "🇦🇷", code: "+54" },
18
+ armenia: { label: "Armenia", flag: "🇦🇲", code: "+374" },
19
+ aruba: { label: "Aruba", flag: "🇦🇼", code: "+297" },
20
+ australia: { label: "Australia", flag: "🇦🇺", code: "+61" },
21
+ austria: { label: "Austria", flag: "🇦🇹", code: "+43" },
22
+ azerbaijan: { label: "Azerbaijan", flag: "🇦🇿", code: "+994" },
23
+ bahamas: { label: "Bahamas", flag: "🇧🇸", code: "+1" },
24
+ bahrain: { label: "Bahrain", flag: "🇧🇭", code: "+973" },
25
+ bangladesh: { label: "Bangladesh", flag: "🇧🇩", code: "+880" },
26
+ barbados: { label: "Barbados", flag: "🇧🇧", code: "+1" },
27
+ belarus: { label: "Belarus", flag: "🇧🇾", code: "+375" },
28
+ belgium: { label: "Belgium", flag: "🇧🇪", code: "+32" },
29
+ belize: { label: "Belize", flag: "🇧🇿", code: "+501" },
30
+ benin: { label: "Benin", flag: "🇧🇯", code: "+229" },
31
+ bhutane: { label: "Bhutan", flag: "🇧🇹", code: "+975" },
32
+ bih: { label: "Bosnia and Herzegovina", flag: "🇧🇦", code: "+387" },
33
+ bolivia: { label: "Bolivia", flag: "🇧🇴", code: "+591" },
34
+ botswana: { label: "Botswana", flag: "🇧🇼", code: "+267" },
35
+ brazil: { label: "Brazil", flag: "🇧🇷", code: "+55" },
36
+ bulgaria: { label: "Bulgaria", flag: "🇧🇬", code: "+359" },
37
+ burkinafaso: { label: "Burkina Faso", flag: "🇧🇫", code: "+226" },
38
+ burundi: { label: "Burundi", flag: "🇧🇮", code: "+257" },
39
+ cambodia: { label: "Cambodia", flag: "🇰🇭", code: "+855" },
40
+ cameroon: { label: "Cameroon", flag: "🇨🇲", code: "+237" },
41
+ canada: { label: "Canada", flag: "🇨🇦", code: "+1" },
42
+ capeverde: { label: "Cape Verde", flag: "🇨🇻", code: "+238" },
43
+ chad: { label: "Chad", flag: "🇹🇩", code: "+235" },
44
+ chile: { label: "Chile", flag: "🇨🇱", code: "+56" },
45
+ colombia: { label: "Colombia", flag: "🇨🇴", code: "+57" },
46
+ comoros: { label: "Comoros", flag: "🇰🇲", code: "+269" },
47
+ congo: { label: "Congo", flag: "🇨🇬", code: "+242" },
48
+ costarica: { label: "Costa Rica", flag: "🇨🇷", code: "+506" },
49
+ croatia: { label: "Croatia", flag: "🇭🇷", code: "+385" },
50
+ cyprus: { label: "Cyprus", flag: "🇨🇾", code: "+357" },
51
+ czech: { label: "Czech Republic", flag: "🇨🇿", code: "+420" },
52
+ denmark: { label: "Denmark", flag: "🇩🇰", code: "+45" },
53
+ djibouti: { label: "Djibouti", flag: "🇩🇯", code: "+253" },
54
+ dominicana: { label: "Dominican Republic", flag: "🇩🇴", code: "+1" },
55
+ easttimor: { label: "East Timor", flag: "🇹🇱", code: "+670" },
56
+ ecuador: { label: "Ecuador", flag: "🇪🇨", code: "+593" },
57
+ egypt: { label: "Egypt", flag: "🇪🇬", code: "+20" },
58
+ england: { label: "England", flag: "🏴", code: "+44" },
59
+ equatorialguinea: { label: "Equatorial Guinea", flag: "🇬🇶", code: "+240" },
60
+ estonia: { label: "Estonia", flag: "🇪🇪", code: "+372" },
61
+ ethiopia: { label: "Ethiopia", flag: "🇪🇹", code: "+251" },
62
+ finland: { label: "Finland", flag: "🇫🇮", code: "+358" },
63
+ france: { label: "France", flag: "🇫🇷", code: "+33" },
64
+ frenchguiana: { label: "French Guiana", flag: "🇬🇫", code: "+594" },
65
+ gabon: { label: "Gabon", flag: "🇬🇦", code: "+241" },
66
+ gambia: { label: "Gambia", flag: "🇬🇲", code: "+220" },
67
+ georgia: { label: "Georgia", flag: "🇬🇪", code: "+995" },
68
+ germany: { label: "Germany", flag: "🇩🇪", code: "+49" },
69
+ ghana: { label: "Ghana", flag: "🇬🇭", code: "+233" },
70
+ gibraltar: { label: "Gibraltar", flag: "🇬🇮", code: "+350" },
71
+ greece: { label: "Greece", flag: "🇬🇷", code: "+30" },
72
+ guadeloupe: { label: "Guadeloupe", flag: "🇬🇵", code: "+590" },
73
+ guatemala: { label: "Guatemala", flag: "🇬🇹", code: "+502" },
74
+ guinea: { label: "Guinea", flag: "🇬🇳", code: "+224" },
75
+ guineabissau: { label: "Guinea-Bissau", flag: "🇬🇼", code: "+245" },
76
+ guyana: { label: "Guyana", flag: "🇬🇾", code: "+592" },
77
+ haiti: { label: "Haiti", flag: "🇭🇹", code: "+509" },
78
+ honduras: { label: "Honduras", flag: "🇭🇳", code: "+504" },
79
+ hongkong: { label: "Hong Kong", flag: "🇭🇰", code: "+852" },
80
+ hungary: { label: "Hungary", flag: "🇭🇺", code: "+36" },
81
+ india: { label: "India", flag: "🇮🇳", code: "+91" },
82
+ indonesia: { label: "Indonesia", flag: "🇮🇩", code: "+62" },
83
+ ireland: { label: "Ireland", flag: "🇮🇪", code: "+353" },
84
+ israel: { label: "Israel", flag: "🇮🇱", code: "+972" },
85
+ italy: { label: "Italy", flag: "🇮🇹", code: "+39" },
86
+ ivorycoast: { label: "Ivory Coast", flag: "🇨🇮", code: "+225" },
87
+ jamaica: { label: "Jamaica", flag: "🇯🇲", code: "+1" },
88
+ jordan: { label: "Jordan", flag: "🇯🇴", code: "+962" },
89
+ kazakhstan: { label: "Kazakhstan", flag: "🇰🇿", code: "+7" },
90
+ kenya: { label: "Kenya", flag: "🇰🇪", code: "+254" },
91
+ kuwait: { label: "Kuwait", flag: "🇰🇼", code: "+965" },
92
+ kyrgyzstan: { label: "Kyrgyzstan", flag: "🇰🇬", code: "+996" },
93
+ laos: { label: "Laos", flag: "🇱🇦", code: "+856" },
94
+ latvia: { label: "Latvia", flag: "🇱🇻", code: "+371" },
95
+ lesotho: { label: "Lesotho", flag: "🇱🇸", code: "+266" },
96
+ liberia: { label: "Liberia", flag: "🇱🇷", code: "+231" },
97
+ lithuania: { label: "Lithuania", flag: "🇱🇹", code: "+370" },
98
+ luxembourg: { label: "Luxembourg", flag: "🇱🇺", code: "+352" },
99
+ macau: { label: "Macau", flag: "🇲🇴", code: "+853" },
100
+ madagascar: { label: "Madagascar", flag: "🇲🇬", code: "+261" },
101
+ malawi: { label: "Malawi", flag: "🇲🇼", code: "+265" },
102
+ malaysia: { label: "Malaysia", flag: "🇲🇾", code: "+60" },
103
+ maldives: { label: "Maldives", flag: "🇲🇻", code: "+960" },
104
+ mauritania: { label: "Mauritania", flag: "🇲🇷", code: "+222" },
105
+ mauritius: { label: "Mauritius", flag: "🇲🇺", code: "+230" },
106
+ mexico: { label: "Mexico", flag: "🇲🇽", code: "+52" },
107
+ moldova: { label: "Moldova", flag: "🇲🇩", code: "+373" },
108
+ mongolia: { label: "Mongolia", flag: "🇲🇳", code: "+976" },
109
+ montenegro: { label: "Montenegro", flag: "🇲🇪", code: "+382" },
110
+ morocco: { label: "Morocco", flag: "🇲🇦", code: "+212" },
111
+ mozambique: { label: "Mozambique", flag: "🇲🇿", code: "+258" },
112
+ namibia: { label: "Namibia", flag: "🇳🇦", code: "+264" },
113
+ nepal: { label: "Nepal", flag: "🇳🇵", code: "+977" },
114
+ netherlands: { label: "Netherlands", flag: "🇳🇱", code: "+31" },
115
+ newcaledonia: { label: "New Caledonia", flag: "🇳🇨", code: "+687" },
116
+ newzealand: { label: "New Zealand", flag: "🇳🇿", code: "+64" },
117
+ nicaragua: { label: "Nicaragua", flag: "🇳🇮", code: "+505" },
118
+ nigeria: { label: "Nigeria", flag: "🇳🇬", code: "+234" },
119
+ northmacedonia: { label: "North Macedonia", flag: "🇲🇰", code: "+389" },
120
+ norway: { label: "Norway", flag: "🇳🇴", code: "+47" },
121
+ oman: { label: "Oman", flag: "🇴🇲", code: "+968" },
122
+ pakistan: { label: "Pakistan", flag: "🇵🇰", code: "+92" },
123
+ panama: { label: "Panama", flag: "🇵🇦", code: "+507" },
124
+ papuanewguinea: { label: "Papua New Guinea", flag: "🇵🇬", code: "+675" },
125
+ paraguay: { label: "Paraguay", flag: "🇵🇾", code: "+595" },
126
+ peru: { label: "Peru", flag: "🇵🇪", code: "+51" },
127
+ philippines: { label: "Philippines", flag: "🇵🇭", code: "+63" },
128
+ poland: { label: "Poland", flag: "🇵🇱", code: "+48" },
129
+ portugal: { label: "Portugal", flag: "🇵🇹", code: "+351" },
130
+ puertorico: { label: "Puerto Rico", flag: "🇵🇷", code: "+1" },
131
+ reunion: { label: "Réunion", flag: "🇷🇪", code: "+262" },
132
+ romania: { label: "Romania", flag: "🇷🇴", code: "+40" },
133
+ russia: { label: "Russia", flag: "🇷🇺", code: "+7" },
134
+ rwanda: { label: "Rwanda", flag: "🇷🇼", code: "+250" },
135
+ saintkittsandnevis: { label: "Saint Kitts and Nevis", flag: "🇰🇳", code: "+1" },
136
+ saintlucia: { label: "Saint Lucia", flag: "🇱🇨", code: "+1" },
137
+ saintvincentandgrenadines: { label: "Saint Vincent and the Grenadines", flag: "🇻🇨", code: "+1" },
138
+ salvador: { label: "El Salvador", flag: "🇸🇻", code: "+503" },
139
+ samoa: { label: "Samoa", flag: "🇼🇸", code: "+685" },
140
+ saudiarabia: { label: "Saudi Arabia", flag: "🇸🇦", code: "+966" },
141
+ senegal: { label: "Senegal", flag: "🇸🇳", code: "+221" },
142
+ serbia: { label: "Serbia", flag: "🇷🇸", code: "+381" },
143
+ seychelles: { label: "Seychelles", flag: "🇸🇨", code: "+248" },
144
+ sierraleone: { label: "Sierra Leone", flag: "🇸🇱", code: "+232" },
145
+ singapore: { label: "Singapore", flag: "🇸🇬", code: "+65" },
146
+ slovakia: { label: "Slovakia", flag: "🇸🇰", code: "+421" },
147
+ slovenia: { label: "Slovenia", flag: "🇸🇮", code: "+386" },
148
+ solomonislands: { label: "Solomon Islands", flag: "🇸🇧", code: "+677" },
149
+ southafrica: { label: "South Africa", flag: "🇿🇦", code: "+27" },
150
+ spain: { label: "Spain", flag: "🇪🇸", code: "+34" },
151
+ srilanka: { label: "Sri Lanka", flag: "🇱🇰", code: "+94" },
152
+ suriname: { label: "Suriname", flag: "🇸🇷", code: "+597" },
153
+ swaziland: { label: "Eswatini", flag: "🇸🇿", code: "+268" },
154
+ sweden: { label: "Sweden", flag: "🇸🇪", code: "+46" },
155
+ switzerland: { label: "Switzerland", flag: "🇨🇭", code: "+41" },
156
+ taiwan: { label: "Taiwan", flag: "🇹🇼", code: "+886" },
157
+ tajikistan: { label: "Tajikistan", flag: "🇹🇯", code: "+992" },
158
+ tanzania: { label: "Tanzania", flag: "🇹🇿", code: "+255" },
159
+ thailand: { label: "Thailand", flag: "🇹🇭", code: "+66" },
160
+ tit: { label: "East Timor", flag: "🇹🇱", code: "+670" },
161
+ togo: { label: "Togo", flag: "🇹🇬", code: "+228" },
162
+ tunisia: { label: "Tunisia", flag: "🇹🇳", code: "+216" },
163
+ turkmenistan: { label: "Turkmenistan", flag: "🇹🇲", code: "+993" },
164
+ uganda: { label: "Uganda", flag: "🇺🇬", code: "+256" },
165
+ ukraine: { label: "Ukraine", flag: "🇺🇦", code: "+380" },
166
+ uruguay: { label: "Uruguay", flag: "🇺🇾", code: "+598" },
167
+ usa: { label: "United States", flag: "🇺🇸", code: "+1" },
168
+ uzbekistan: { label: "Uzbekistan", flag: "🇺🇿", code: "+998" },
169
+ venezuela: { label: "Venezuela", flag: "🇻🇪", code: "+58" },
170
+ vietnam: { label: "Vietnam", flag: "🇻🇳", code: "+84" },
171
+ zambia: { label: "Zambia", flag: "🇿🇲", code: "+260" }
172
+ };
src/bots/utils/handlerUtils.ts ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // utils/handlerUtils.ts
2
+
3
+ import { BotContext } from "../types/botTypes";
4
+ import { createLogger } from '../../utils/logger';
5
+ import { AuthService } from '../services/auth';
6
+ import { messageManager } from './messageManager';
7
+ import { getMainMenuKeyboard } from './keyboardUtils';
8
+
9
+ const logger = createLogger('HandlerUtils');
10
+ const authService = AuthService.getInstance();
11
+
12
+ /**
13
+ * Creates a standardized message reply handler with error handling
14
+ * @param replyFunction Function that returns the message and options to send
15
+ * @returns A function that handles the reply with proper error handling
16
+ */
17
+ export const messageReplyHandler = (
18
+ replyFunction: (ctx: BotContext) => Promise<{message: string, options?: any}> | {message: string, options?: any}
19
+ ) => async (ctx: BotContext) => {
20
+ try {
21
+ // Execute the reply function to get message content and options
22
+ const result = await Promise.resolve(replyFunction(ctx));
23
+
24
+ // Get the message ID to reply to
25
+ const replyToMessageId = ctx.message?.message_id;
26
+
27
+ // Define default options including parse_mode
28
+ const defaultOptions = {
29
+ parse_mode: 'HTML'
30
+ };
31
+
32
+ // Add reply_to_message_id to options if not already set
33
+ const finalOptions = {
34
+ ...defaultOptions,
35
+ ...result.options,
36
+ reply_to_message_id: replyToMessageId
37
+ };
38
+
39
+ // Send the reply with the message and options
40
+ return await ctx.reply(result.message, finalOptions);
41
+ } catch (error: any) {
42
+ // Log the error
43
+ logger.error(`Error in message reply handler: ${error.message}`);
44
+
45
+ // Send a generic error message to the user
46
+ return ctx.reply("⚠️ حدث خطأ أثناء معالجة طلبك. الرجاء المحاولة مرة أخرى.");
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Creates a standardized callback query handler with error handling
52
+ * @param replyFunction Function that returns the message and options to send after answering callback
53
+ * @returns A function that handles the callback with proper error handling
54
+ */
55
+ export const callbackReplyHandler = (
56
+ replyFunction: (ctx: BotContext) => Promise<{message: string, options?: any}> | {message: string, options?: any}
57
+ ) => async (ctx: BotContext) => {
58
+ try {
59
+ // Answer the callback query first
60
+ await ctx.answerCbQuery();
61
+
62
+
63
+ // Execute the reply function to get message content and options
64
+ const result = await Promise.resolve(replyFunction(ctx));
65
+
66
+ // Send the reply with the message and any provided options
67
+ return await ctx.reply(result.message, result.options);
68
+ } catch (error: any) {
69
+ // Log the error
70
+ logger.error(`Error in callback reply handler: ${error.message}`);
71
+
72
+ // Send a generic error message to the user
73
+ return ctx.reply("⚠️ حدث خطأ أثناء معالجة طلبك. الرجاء المحاولة مرة أخرى.");
74
+ }
75
+ };
76
+
77
+ export const authCheckHandler = <T extends any[]>(
78
+ handler: (ctx: BotContext, ...args: T) => Promise<{message: string, options?: any}> | {message: string, options?: any}
79
+ ) => async (ctx: BotContext, ...args: T) => {
80
+ const telegramId = ctx.from?.id;
81
+
82
+ if (!telegramId || !authService.isUserLoggedIn(telegramId, ctx)) {
83
+ return {
84
+ message: messageManager.getMessage('auth_required'),
85
+ options: getMainMenuKeyboard()
86
+ };
87
+ }
88
+
89
+ return handler(ctx, ...args);
90
+ };
src/bots/utils/keyboardUtils.ts ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import fiveSimProducts from "./5sim_products.json";
7
+ import { VirtualNumberService } from "../services/VirtualNumberService";
8
+ import { createLogger } from "../../utils/logger";
9
+
10
+ const logger = createLogger('KeyboardUtils');
11
+
12
+ export const getMainMenuKeyboard = () => {
13
+ return Markup.inlineKeyboard([
14
+ [Markup.button.callback(messageManager.getMessage('btn_login'), 'login')],
15
+ [Markup.button.callback(messageManager.getMessage('btn_terms'), 'terms')],
16
+ [Markup.button.callback(messageManager.getMessage('btn_new_members'), 'new_members')],
17
+ // [Markup.button.callback(messageManager.getMessage('btn_stats'), 'stats')],
18
+ [Markup.button.callback(messageManager.getMessage('btn_change_language'), 'change_language')],
19
+ ]);
20
+ };
21
+
22
+ export const getLoggedInMenuKeyboard = () => {
23
+ return Markup.inlineKeyboard([
24
+ [Markup.button.callback('🔍 Browse Services', 'browse_services')],
25
+ [
26
+ Markup.button.callback(messageManager.getMessage('btn_profile'), 'profile'),
27
+ Markup.button.callback(messageManager.getMessage('btn_change_language'), 'change_language')
28
+ ],
29
+ [
30
+ Markup.button.callback(messageManager.getMessage('btn_top_up'), 'top_up_balance'),
31
+ Markup.button.callback(messageManager.getMessage('btn_history'), 'history')
32
+ ],
33
+ // [Markup.button.callback('💰 Buy with Balance', 'buy_with_balance')],
34
+ [
35
+ Markup.button.callback(messageManager.getMessage('btn_back'), 'main_menu')
36
+ ],
37
+ ]);
38
+ };
39
+
40
+ export const getServicesKeyboard = (page: number = 0, sortBy?: 'az' | 'za') => {
41
+ const buttons = [];
42
+ const services = Object.entries(fiveSimProducts);
43
+ const rowSize = 2; // 2 buttons per row
44
+ const servicesPerPage = 20;
45
+
46
+
47
+ // Apply sorting for all services first
48
+ let sortedServices = [...services];
49
+ if (sortBy === 'az') {
50
+
51
+ sortedServices.sort((a, b) => a[1].label_en.localeCompare(b[1].label_en));
52
+ } else if (sortBy === 'za') {
53
+ sortedServices.sort((a, b) => b[1].label_en.localeCompare(a[1].label_en));
54
+ }
55
+
56
+ const totalPages = Math.ceil(sortedServices.length / servicesPerPage);
57
+
58
+ // Get services for current page from the sorted array
59
+ const startIndex = page * servicesPerPage;
60
+ const endIndex = Math.min(startIndex + servicesPerPage, sortedServices.length);
61
+ const pageServices = sortedServices.slice(startIndex, endIndex);
62
+
63
+ // Add sort button
64
+ const sortButtonText = sortBy === 'az' ? 'Sort Z-A ⬇️' : 'Sort A-Z ⬆️';
65
+ const sortButtonCallback = sortBy === 'az' ? `sort_services_za_${page}` : `sort_services_az_${page}`;
66
+ buttons.push([Markup.button.callback(sortButtonText, sortButtonCallback)]);
67
+
68
+ // Generate service buttons in pairs
69
+ for (let i = 0; i < pageServices.length; i += rowSize) {
70
+ const row = [];
71
+ for (let j = 0; j < rowSize && i + j < pageServices.length; j++) {
72
+ const [serviceId, serviceData] = pageServices[i + j];
73
+ row.push(
74
+ Markup.button.callback(
75
+ `${serviceData.icon} ${serviceData.label_en}`,
76
+ `service_${serviceId}`
77
+ )
78
+ );
79
+ }
80
+ buttons.push(row);
81
+ }
82
+
83
+ // Add pagination buttons
84
+ const paginationRow = [];
85
+ if (page > 0) {
86
+ paginationRow.push(
87
+ Markup.button.callback(
88
+ '⬅️ Previous',
89
+ `services_page_${page - 1}${sortBy ? `_${sortBy}` : ''}`
90
+ )
91
+ );
92
+ }
93
+
94
+ paginationRow.push(
95
+ Markup.button.callback(
96
+ `📄 ${page + 1}/${totalPages}`,
97
+ 'noop'
98
+ )
99
+ );
100
+
101
+ if (page < totalPages - 1) {
102
+ paginationRow.push(
103
+ Markup.button.callback(
104
+ 'Next ➡️',
105
+ `services_page_${page + 1}${sortBy ? `_${sortBy}` : ''}`
106
+ )
107
+ );
108
+ }
109
+
110
+ if (paginationRow.length > 0) {
111
+ buttons.push(paginationRow);
112
+ }
113
+
114
+ // Add search button
115
+ buttons.push([Markup.button.callback('🔍 Search Product', 'search_product')]);
116
+
117
+ // Add back button to return to main menu
118
+ buttons.push([
119
+ Markup.button.callback('🔙 Back to Menu', 'logged_in_menu')
120
+ ]);
121
+
122
+ return Markup.inlineKeyboard(buttons);
123
+ };
124
+
125
+ export const getServicesPaginationInfo = (page: number = 0) => {
126
+ const services = Object.entries(fiveSimProducts);
127
+ const servicesPerPage = 10;
128
+ const totalPages = Math.ceil(services.length / servicesPerPage);
129
+ const startIndex = page * servicesPerPage;
130
+ const endIndex = Math.min(startIndex + servicesPerPage, services.length);
131
+
132
+ return `<b>Services List</b>\n` +
133
+ `📋 Showing services ${startIndex + 1}-${endIndex} of ${services.length}\n` +
134
+ `📄 Page ${page + 1} of ${totalPages}`;
135
+ };
136
+
137
+ export const getBackToMainMenuButton = () => {
138
+ return Markup.inlineKeyboard([
139
+ [Markup.button.callback(messageManager.getMessage('btn_back'), 'main_menu')]
140
+ ]);
141
+ };
142
+
143
+ export const mapApiCountriesToButtons = (apiCountries: any[], service: string) => {
144
+ const buttons = [];
145
+ const rowSize = 2; // 2 buttons per row
146
+
147
+ // Sort countries by name for better UX
148
+ const sortedCountries = apiCountries.sort((a, b) => a.name.localeCompare(b.name));
149
+
150
+ for (let i = 0; i < sortedCountries.length; i += rowSize) {
151
+ const row = [];
152
+ for (let j = 0; j < rowSize && i + j < sortedCountries.length; j++) {
153
+ const country = sortedCountries[i + j];
154
+ const countryInfo = countryData[country.id.toLowerCase()];
155
+ if (countryInfo) {
156
+ row.push(
157
+ Markup.button.callback(
158
+ `${countryInfo.label} ${countryInfo.flag} ${countryInfo.code}`,
159
+ `country_${service}_${country.id}`
160
+ )
161
+ );
162
+ }
163
+ }
164
+ if (row.length > 0) {
165
+ buttons.push(row);
166
+ }
167
+ }
168
+
169
+ return buttons;
170
+ };
171
+
172
+ export const getCountriesKeyboard = async (service: string, page: number = 0) => {
173
+ const buttons = [];
174
+ const countriesPerPage = 15;
175
+
176
+ try {
177
+ // Get countries from API
178
+ const virtualNumberService = VirtualNumberService.getInstance();
179
+ const apiCountries = await virtualNumberService.getAvailableCountries(service);
180
+
181
+ const mappedButtons = mapApiCountriesToButtons(apiCountries, service);
182
+ const totalPages = Math.ceil(mappedButtons.length / countriesPerPage);
183
+ const startIndex = page * countriesPerPage;
184
+ const endIndex = Math.min(startIndex + countriesPerPage, mappedButtons.length);
185
+ const pageButtons = mappedButtons.slice(startIndex, endIndex);
186
+
187
+ buttons.push(...pageButtons);
188
+
189
+ const paginationRow = [];
190
+ if (page > 0) {
191
+ paginationRow.push(
192
+ Markup.button.callback(
193
+ messageManager.getMessage('btn_previous'),
194
+ `page_${service}_${page - 1}`
195
+ )
196
+ );
197
+ }
198
+
199
+ paginationRow.push(
200
+ Markup.button.callback(
201
+ messageManager.getMessage('btn_page_info')
202
+ .replace('{current}', (page + 1).toString())
203
+ .replace('{total}', totalPages.toString()),
204
+ 'noop'
205
+ )
206
+ );
207
+
208
+ if (page < totalPages - 1) {
209
+ paginationRow.push(
210
+ Markup.button.callback(
211
+ messageManager.getMessage('btn_next'),
212
+ `page_${service}_${page + 1}`
213
+ )
214
+ );
215
+ }
216
+
217
+ if (paginationRow.length > 0) {
218
+ buttons.push(paginationRow);
219
+ }
220
+
221
+ buttons.push([
222
+ Markup.button.callback(
223
+ messageManager.getMessage('btn_main_menu'),
224
+ 'main_menu'
225
+ )
226
+ ]);
227
+ } catch (error: any) {
228
+ logger.error(`Error fetching countries: ${error.message}`);
229
+ buttons.push([
230
+ Markup.button.callback(
231
+ messageManager.getMessage('btn_error_loading_countries'),
232
+ 'main_menu'
233
+ )
234
+ ]);
235
+ }
236
+
237
+ return Markup.inlineKeyboard(buttons);
238
+ };
239
+
240
+ export const getServicePricesKeyboard = (
241
+ prices: any,
242
+ service: string,
243
+ country: string,
244
+ ctx: BotContext
245
+ ) => {
246
+ const buttons = [];
247
+
248
+ const hasValidPrices = prices && Object.values(prices).some(
249
+ (priceInfo: any) => priceInfo && priceInfo.count > 0
250
+ );
251
+
252
+ if (!hasValidPrices) {
253
+ buttons.push([
254
+ Markup.button.callback(
255
+ messageManager.getMessage('btn_no_prices'),
256
+ 'noop'
257
+ ),
258
+ ]);
259
+ } else {
260
+ Object.entries(prices).forEach(([operator, priceInfo]: [string, any]) => {
261
+ if (priceInfo.count > 0) {
262
+ buttons.push([
263
+ Markup.button.callback(
264
+ messageManager.getMessage('btn_buy_format')
265
+ .replace('{price}', priceInfo.cost.toFixed(2))
266
+ .replace('{count}', priceInfo.count.toString()),
267
+ `buy_${service}_${country}_${operator}`
268
+ ),
269
+ ]);
270
+ }
271
+ });
272
+ }
273
+
274
+ buttons.push([
275
+ Markup.button.callback(
276
+ messageManager.getMessage('btn_back_to_services'),
277
+ `service_${service}`
278
+ ),
279
+ ]);
280
+
281
+ buttons.push([
282
+ Markup.button.callback(
283
+ messageManager.getMessage('btn_main_menu'),
284
+ 'main_menu'
285
+ )
286
+ ]);
287
+
288
+ return Markup.inlineKeyboard(buttons);
289
+ };
290
+
291
+ export const getHistoryKeyboard = () => {
292
+ return Markup.inlineKeyboard([
293
+ [Markup.button.callback(
294
+ messageManager.getMessage('btn_numbers_history'),
295
+ 'numbers_history'
296
+ )],
297
+ [Markup.button.callback(
298
+ messageManager.getMessage('btn_purchases_history'),
299
+ 'purchases_history'
300
+ )],
301
+ [Markup.button.callback(
302
+ messageManager.getMessage('btn_back_to_main'),
303
+ 'main_menu'
304
+ )]
305
+ ]);
306
+ };
307
+
308
+ export const getLanguageSelectionKeyboard = (isLoggedIn: boolean = false) => {
309
+ const buttons = [
310
+ [
311
+ Markup.button.callback(
312
+ messageManager.getMessage('btn_lang_english'),
313
+ 'set_language_en'
314
+ ),
315
+ Markup.button.callback(
316
+ messageManager.getMessage('btn_lang_arabic'),
317
+ 'set_language_ar'
318
+ )
319
+ ]
320
+ ];
321
+
322
+ buttons.push([
323
+ Markup.button.callback(
324
+ messageManager.getMessage('btn_back'),
325
+ isLoggedIn ? 'logged_in_menu' : 'main_menu'
326
+ )
327
+ ]);
328
+
329
+ return Markup.inlineKeyboard(buttons);
330
+ };
331
+
332
+ export const getProfileKeyboard = () => {
333
+ return Markup.inlineKeyboard([
334
+ [Markup.button.callback(
335
+ messageManager.getMessage('btn_change_email'),
336
+ 'change_email'
337
+ )],
338
+ [Markup.button.callback(
339
+ messageManager.getMessage('btn_change_password'),
340
+ 'change_password'
341
+ )],
342
+ [Markup.button.callback(
343
+ messageManager.getMessage('btn_account_info'),
344
+ 'account_info'
345
+ )],
346
+ // [Markup.button.callback(
347
+ // messageManager.getMessage('btn_gift_balance'),
348
+ // 'gift_balance'
349
+ // )],
350
+ [Markup.button.callback(
351
+ messageManager.getMessage('btn_back_to_logged_in'),
352
+ 'logged_in_menu'
353
+ )]
354
+ ]);
355
+ };
src/bots/utils/messageManager.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ setLanguage(lang: 'ar' | 'en') {
32
+ this.language = lang;
33
+ }
34
+
35
+ async loadMessages() {
36
+ try {
37
+ const { data, error } = await supabase
38
+ .from('bot_messages')
39
+ .select('*')
40
+
41
+ if (error) throw error;
42
+
43
+ this.messages.clear();
44
+ data.forEach((message: BotMessage) => {
45
+ this.messages.set(message.key, message);
46
+ });
47
+
48
+ // Add default messages if not in database
49
+ this.addDefaultMessages();
50
+
51
+ logger.info(`Messages loaded successfully for bot `);
52
+ } catch (error) {
53
+ logger.error('Error loading messages:', error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ private addDefaultMessages() {
59
+ const defaultMessages = {
60
+ 'balance_update_notification': {
61
+ ar_value: '💰 <b>تحديث الرصيد</b>\n\n' +
62
+ 'تم تحديث رصيدك:\n' +
63
+ 'الرصيد السابق: {old_balance}$\n' +
64
+ 'الرصيد الجديد: {new_balance}$\n' +
65
+ 'نوع التغيير: {change_type}\n' +
66
+ 'قيمة التغيير: {change_amount}$',
67
+ en_value: '💰 <b>Balance Update</b>\n\n' +
68
+ 'Your balance has been updated:\n' +
69
+ 'Previous balance: ${old_balance}\n' +
70
+ 'New balance: ${new_balance}\n' +
71
+ 'Change type: {change_type}\n' +
72
+ 'Change amount: ${change_amount}'
73
+ },
74
+ 'search_product_prompt': {
75
+ ar_value: 'الرجاء إدخال اسم الخدمة التي تبحث عنها:',
76
+ en_value: 'Please enter the name of the service you are looking for:'
77
+ },
78
+ 'no_search_results': {
79
+ ar_value: 'عذراً، لم يتم العثور على نتائج لبحثك.',
80
+ en_value: 'Sorry, no results found for your search.'
81
+ },
82
+ 'invalid_search_input': {
83
+ ar_value: 'الرجاء إدخال ما لا يقل عن حرفين للبحث.',
84
+ en_value: 'Please enter at least 2 characters to search.'
85
+ },
86
+ 'search_country_prompt': {
87
+ ar_value: 'الرجاء إدخال اسم الدولة أو رمزها أو علمها:',
88
+ en_value: 'Please enter the country name, code, or flag:'
89
+ },
90
+ 'no_country_search_results': {
91
+ ar_value: 'عذراً، لم يتم العثور على دول مطابقة لبحثك.',
92
+ en_value: 'Sorry, no matching countries found for your search.'
93
+ },
94
+ 'affordable_products_list': {
95
+ ar_value: 'المنتجات التي يمكنك شراؤها برصيدك الحالي ({balance}):',
96
+ en_value: 'Products you can buy with your current balance ({balance}):'
97
+ },
98
+ 'no_affordable_products': {
99
+ ar_value: 'عذراً، لا توجد منتجات يمكنك شراؤها برصيدك الحالي ({balance}).',
100
+ en_value: 'Sorry, no products can be bought with your current balance ({balance}).'
101
+ }
102
+ };
103
+
104
+ Object.entries(defaultMessages).forEach(([key, value]) => {
105
+ if (!this.messages.has(key)) {
106
+ this.messages.set(key, {
107
+ id: key,
108
+ bot_id: 'default',
109
+ key,
110
+ ar_value: value.ar_value,
111
+ en_value: value.en_value,
112
+ created_at: new Date().toISOString(),
113
+ updated_at: new Date().toISOString()
114
+ });
115
+ }
116
+ });
117
+ }
118
+
119
+ getMessage(key: string): string {
120
+ const message = this.messages.get(key);
121
+ if (!message) {
122
+ logger.warn(`Message not found for key: ${key}`);
123
+ return `${key}`;
124
+ }
125
+ return this.language === 'ar' ? message.ar_value : message.en_value;
126
+ }
127
+
128
+ async updateMessage(key: string, arValue: string, enValue: string, description?: string) {
129
+ try {
130
+ const { data, error } = await supabase
131
+ .from('bot_messages')
132
+ .upsert({
133
+ key,
134
+ ar_value: arValue,
135
+ en_value: enValue,
136
+ description
137
+ })
138
+ .select()
139
+ .single();
140
+
141
+ if (error) throw error;
142
+
143
+ if (data) {
144
+ this.messages.set(key, data);
145
+ }
146
+
147
+ return data;
148
+ } catch (error) {
149
+ logger.error('Error updating message:', error);
150
+ throw error;
151
+ }
152
+ }
153
+ }
154
+
155
+ export const messageManager = MessageManager.getInstance();