Spaces:
Paused
Paused
Mohammed Foud commited on
Commit ·
959b027
1
Parent(s): ab38ff2
all
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .commitlintrc.json +3 -0
- .cursorignore +4 -0
- .dockerignore +7 -0
- .editorconfig +11 -0
- .eslintignore +2 -0
- .eslintrc.cjs +4 -0
- .gitattributes +1 -0
- .gitignore +39 -0
- .npmrc +1 -0
- .vscode/extensions.json +3 -0
- .vscode/settings.json +21 -0
- Dockerfile +29 -0
- a.py +13 -0
- d.sh +7 -0
- db.sql +185 -0
- login_failed.png +3 -0
- package.json +55 -0
- pnpm-lock.yaml +0 -0
- run.sh +4 -0
- src/api/paymentWebhooks.ts +47 -0
- src/bots/botManager.ts +234 -0
- src/bots/db.sql +153 -0
- src/bots/handlers/balanceHandlers.ts +252 -0
- src/bots/handlers/commandHandlers.ts +150 -0
- src/bots/handlers/giftHandlers.ts +175 -0
- src/bots/handlers/historyHandlers.ts +155 -0
- src/bots/handlers/index.ts +45 -0
- src/bots/handlers/languageHandlers.ts +74 -0
- src/bots/handlers/mainMenuHandlers.ts +139 -0
- src/bots/handlers/paymentWebhookHandlers.ts +47 -0
- src/bots/handlers/profileHandlers.ts +320 -0
- src/bots/handlers/purchaseHandlers.ts +241 -0
- src/bots/handlers/serviceHandlers.ts +878 -0
- src/bots/index.ts +112 -0
- src/bots/middleware/groupCheckMiddleware.ts +52 -0
- src/bots/services/AdminService.ts +24 -0
- src/bots/services/BalanceUpdateService.ts +266 -0
- src/bots/services/CryptoService.ts +44 -0
- src/bots/services/PayPalService.ts +121 -0
- src/bots/services/PaymentVerificationService.ts +83 -0
- src/bots/services/PurchaseTrackingService.ts +174 -0
- src/bots/services/VirtualNumberService.ts +176 -0
- src/bots/services/auth.ts +226 -0
- src/bots/types/botTypes.ts +17 -0
- src/bots/types/paymentTypes.ts +11 -0
- src/bots/utils/5sim_products.json +0 -0
- src/bots/utils/country.ts +172 -0
- src/bots/utils/handlerUtils.ts +90 -0
- src/bots/utils/keyboardUtils.ts +355 -0
- 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
|
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();
|