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