alphabagibagi commited on
Commit
b56f837
·
verified ·
1 Parent(s): 8e7219c

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +5 -1
  2. admin.py +421 -0
  3. bot.py +682 -682
  4. requirements.txt +5 -4
  5. start.sh +8 -0
Dockerfile CHANGED
@@ -8,4 +8,8 @@ RUN pip install --no-cache-dir -r requirements.txt
8
 
9
  COPY . .
10
 
11
- CMD ["python", "bot.py"]
 
 
 
 
 
8
 
9
  COPY . .
10
 
11
+ # Make startup script executable
12
+ RUN chmod +x start.sh
13
+
14
+ # Run both bot and admin console
15
+ CMD ["./start.sh"]
admin.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from flask import Flask, render_template_string, request, redirect, url_for, flash, session
4
+ from functools import wraps
5
+ from supabase import create_client, Client
6
+ from dotenv import load_dotenv
7
+ import requests
8
+
9
+ # Load environment variables
10
+ load_dotenv()
11
+
12
+ # Configuration
13
+ TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
14
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
15
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
16
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123") # Set this in your .env
17
+ TELEGRAM_API_BASE = os.getenv("TELEGRAM_API_BASE_URL", "https://api.telegram.org")
18
+
19
+ # Supabase Client
20
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
21
+
22
+ app = Flask(__name__)
23
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", "your-secret-key-change-this")
24
+
25
+ # HTML Templates
26
+ LOGIN_TEMPLATE = """
27
+ <!DOCTYPE html>
28
+ <html>
29
+ <head>
30
+ <title>Admin Login - Icebox AI</title>
31
+ <meta name="viewport" content="width=device-width, initial-scale=1">
32
+ <style>
33
+ * { box-sizing: border-box; margin: 0; padding: 0; }
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
36
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
37
+ min-height: 100vh;
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ padding: 20px;
42
+ }
43
+ .login-box {
44
+ background: rgba(255,255,255,0.1);
45
+ backdrop-filter: blur(10px);
46
+ border-radius: 20px;
47
+ padding: 40px;
48
+ width: 100%;
49
+ max-width: 400px;
50
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
51
+ }
52
+ h1 { color: #fff; text-align: center; margin-bottom: 30px; }
53
+ .form-group { margin-bottom: 20px; }
54
+ label { color: #aaa; display: block; margin-bottom: 8px; }
55
+ input[type="password"] {
56
+ width: 100%;
57
+ padding: 15px;
58
+ border: none;
59
+ border-radius: 10px;
60
+ background: rgba(255,255,255,0.1);
61
+ color: #fff;
62
+ font-size: 16px;
63
+ }
64
+ input[type="password"]:focus { outline: 2px solid #667eea; }
65
+ button {
66
+ width: 100%;
67
+ padding: 15px;
68
+ border: none;
69
+ border-radius: 10px;
70
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
71
+ color: #fff;
72
+ font-size: 16px;
73
+ font-weight: bold;
74
+ cursor: pointer;
75
+ transition: transform 0.2s;
76
+ }
77
+ button:hover { transform: scale(1.02); }
78
+ .error { color: #ff6b6b; text-align: center; margin-bottom: 20px; }
79
+ </style>
80
+ </head>
81
+ <body>
82
+ <div class="login-box">
83
+ <h1>🔐 Admin Login</h1>
84
+ {% if error %}<p class="error">{{ error }}</p>{% endif %}
85
+ <form method="POST">
86
+ <div class="form-group">
87
+ <label>Password</label>
88
+ <input type="password" name="password" required autofocus>
89
+ </div>
90
+ <button type="submit">Login</button>
91
+ </form>
92
+ </div>
93
+ </body>
94
+ </html>
95
+ """
96
+
97
+ DASHBOARD_TEMPLATE = """
98
+ <!DOCTYPE html>
99
+ <html>
100
+ <head>
101
+ <title>Admin Console - Icebox AI</title>
102
+ <meta name="viewport" content="width=device-width, initial-scale=1">
103
+ <style>
104
+ * { box-sizing: border-box; margin: 0; padding: 0; }
105
+ body {
106
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
107
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
108
+ min-height: 100vh;
109
+ color: #fff;
110
+ }
111
+ .header {
112
+ background: rgba(255,255,255,0.1);
113
+ padding: 20px;
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: center;
117
+ }
118
+ .header h1 { font-size: 24px; }
119
+ .logout-btn {
120
+ background: rgba(255,255,255,0.2);
121
+ color: #fff;
122
+ border: none;
123
+ padding: 10px 20px;
124
+ border-radius: 8px;
125
+ cursor: pointer;
126
+ text-decoration: none;
127
+ }
128
+ .container { max-width: 1200px; margin: 0 auto; padding: 30px 20px; }
129
+ .stats-grid {
130
+ display: grid;
131
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
132
+ gap: 20px;
133
+ margin-bottom: 30px;
134
+ }
135
+ .stat-card {
136
+ background: rgba(255,255,255,0.1);
137
+ backdrop-filter: blur(10px);
138
+ border-radius: 15px;
139
+ padding: 25px;
140
+ text-align: center;
141
+ }
142
+ .stat-card h3 { font-size: 36px; margin-bottom: 10px; }
143
+ .stat-card p { color: #aaa; }
144
+ .card {
145
+ background: rgba(255,255,255,0.1);
146
+ backdrop-filter: blur(10px);
147
+ border-radius: 15px;
148
+ padding: 25px;
149
+ margin-bottom: 20px;
150
+ }
151
+ .card h2 { margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 15px; }
152
+ .form-group { margin-bottom: 20px; }
153
+ label { display: block; margin-bottom: 8px; color: #aaa; }
154
+ select, textarea, input[type="text"] {
155
+ width: 100%;
156
+ padding: 15px;
157
+ border: none;
158
+ border-radius: 10px;
159
+ background: rgba(255,255,255,0.1);
160
+ color: #fff;
161
+ font-size: 14px;
162
+ font-family: inherit;
163
+ }
164
+ textarea { min-height: 150px; resize: vertical; }
165
+ select option { background: #1a1a2e; }
166
+ .btn {
167
+ padding: 15px 30px;
168
+ border: none;
169
+ border-radius: 10px;
170
+ font-size: 16px;
171
+ font-weight: bold;
172
+ cursor: pointer;
173
+ transition: transform 0.2s;
174
+ }
175
+ .btn:hover { transform: scale(1.02); }
176
+ .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
177
+ .btn-success { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: #fff; }
178
+ .alert {
179
+ padding: 15px;
180
+ border-radius: 10px;
181
+ margin-bottom: 20px;
182
+ }
183
+ .alert-success { background: rgba(56, 239, 125, 0.2); color: #38ef7d; }
184
+ .alert-error { background: rgba(255, 107, 107, 0.2); color: #ff6b6b; }
185
+ .user-list {
186
+ max-height: 300px;
187
+ overflow-y: auto;
188
+ background: rgba(0,0,0,0.2);
189
+ border-radius: 10px;
190
+ padding: 15px;
191
+ }
192
+ .user-item {
193
+ display: flex;
194
+ justify-content: space-between;
195
+ padding: 10px;
196
+ border-bottom: 1px solid rgba(255,255,255,0.1);
197
+ }
198
+ .user-item:last-child { border-bottom: none; }
199
+ .checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; }
200
+ .checkbox-item {
201
+ background: rgba(255,255,255,0.1);
202
+ padding: 10px 15px;
203
+ border-radius: 8px;
204
+ cursor: pointer;
205
+ }
206
+ .checkbox-item:hover { background: rgba(255,255,255,0.2); }
207
+ .checkbox-item input { margin-right: 8px; }
208
+ </style>
209
+ </head>
210
+ <body>
211
+ <div class="header">
212
+ <h1>🤖 Icebox AI Admin</h1>
213
+ <a href="/logout" class="logout-btn">Logout</a>
214
+ </div>
215
+
216
+ <div class="container">
217
+ {% with messages = get_flashed_messages(with_categories=true) %}
218
+ {% for category, message in messages %}
219
+ <div class="alert alert-{{ category }}">{{ message }}</div>
220
+ {% endfor %}
221
+ {% endwith %}
222
+
223
+ <div class="stats-grid">
224
+ <div class="stat-card">
225
+ <h3>{{ total_users }}</h3>
226
+ <p>Total Users</p>
227
+ </div>
228
+ <div class="stat-card">
229
+ <h3>{{ active_users }}</h3>
230
+ <p>Active Users</p>
231
+ </div>
232
+ <div class="stat-card">
233
+ <h3>{{ total_generations }}</h3>
234
+ <p>Total Generations</p>
235
+ </div>
236
+ </div>
237
+
238
+ <div class="card">
239
+ <h2>📢 Send Broadcast Message</h2>
240
+ <form method="POST" action="/broadcast">
241
+ <div class="form-group">
242
+ <label>Target Audience</label>
243
+ <select name="target">
244
+ <option value="all">All Users</option>
245
+ <option value="active">Active Users (last 7 days)</option>
246
+ <option value="specific">Specific User (by Chat ID)</option>
247
+ </select>
248
+ </div>
249
+ <div class="form-group" id="chatIdGroup" style="display:none;">
250
+ <label>Chat ID (comma separated for multiple)</label>
251
+ <input type="text" name="chat_ids" placeholder="123456789, 987654321">
252
+ </div>
253
+ <div class="form-group">
254
+ <label>Message (supports Markdown)</label>
255
+ <textarea name="message" placeholder="**New Update!** 🎉
256
+
257
+ We've added exciting new features:
258
+ • Image to Image generation
259
+ • Multi-image support
260
+
261
+ Try it now!"></textarea>
262
+ </div>
263
+ <button type="submit" class="btn btn-primary">Send Broadcast</button>
264
+ </form>
265
+ </div>
266
+
267
+ <div class="card">
268
+ <h2>👥 Recent Users</h2>
269
+ <div class="user-list">
270
+ {% for user in users %}
271
+ <div class="user-item">
272
+ <span>
273
+ <strong>{{ user.first_name or 'Unknown' }}</strong>
274
+ {% if user.username %}(@{{ user.username }}){% endif %}
275
+ </span>
276
+ <span style="color: #aaa;">{{ user.chat_id }}</span>
277
+ </div>
278
+ {% endfor %}
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ <script>
284
+ document.querySelector('select[name="target"]').addEventListener('change', function() {
285
+ document.getElementById('chatIdGroup').style.display =
286
+ this.value === 'specific' ? 'block' : 'none';
287
+ });
288
+ </script>
289
+ </body>
290
+ </html>
291
+ """
292
+
293
+ def login_required(f):
294
+ @wraps(f)
295
+ def decorated_function(*args, **kwargs):
296
+ if not session.get('logged_in'):
297
+ return redirect(url_for('login'))
298
+ return f(*args, **kwargs)
299
+ return decorated_function
300
+
301
+
302
+ @app.route('/login', methods=['GET', 'POST'])
303
+ def login():
304
+ error = None
305
+ if request.method == 'POST':
306
+ if request.form['password'] == ADMIN_PASSWORD:
307
+ session['logged_in'] = True
308
+ return redirect(url_for('dashboard'))
309
+ else:
310
+ error = 'Invalid password'
311
+ return render_template_string(LOGIN_TEMPLATE, error=error)
312
+
313
+
314
+ @app.route('/logout')
315
+ def logout():
316
+ session.pop('logged_in', None)
317
+ return redirect(url_for('login'))
318
+
319
+
320
+ @app.route('/')
321
+ @login_required
322
+ def dashboard():
323
+ # Get stats
324
+ try:
325
+ total_users = supabase.table("telegram_users").select("id", count="exact").execute().count
326
+ active_users = supabase.table("telegram_users").select("id", count="exact").gte(
327
+ "last_active",
328
+ (datetime.now() - timedelta(days=7)).isoformat()
329
+ ).execute().count if total_users else 0
330
+ total_generations = supabase.table("image_generation_logs").select("id", count="exact").execute().count
331
+
332
+ # Get recent users
333
+ users = supabase.table("telegram_users").select("chat_id, username, first_name").order(
334
+ "last_active", desc=True
335
+ ).limit(20).execute().data
336
+ except Exception as e:
337
+ total_users = 0
338
+ active_users = 0
339
+ total_generations = 0
340
+ users = []
341
+ flash(f'Database error: {str(e)}', 'error')
342
+
343
+ return render_template_string(
344
+ DASHBOARD_TEMPLATE,
345
+ total_users=total_users or 0,
346
+ active_users=active_users or 0,
347
+ total_generations=total_generations or 0,
348
+ users=users or []
349
+ )
350
+
351
+
352
+ def send_telegram_message(chat_id: int, message: str) -> bool:
353
+ """Send a message to a specific chat using Telegram API."""
354
+ try:
355
+ url = f"{TELEGRAM_API_BASE}/bot{TELEGRAM_TOKEN}/sendMessage"
356
+ data = {
357
+ "chat_id": chat_id,
358
+ "text": message,
359
+ "parse_mode": "Markdown"
360
+ }
361
+ response = requests.post(url, json=data, timeout=10)
362
+ return response.status_code == 200
363
+ except Exception as e:
364
+ print(f"Error sending to {chat_id}: {e}")
365
+ return False
366
+
367
+
368
+ @app.route('/broadcast', methods=['POST'])
369
+ @login_required
370
+ def broadcast():
371
+ target = request.form.get('target', 'all')
372
+ message = request.form.get('message', '').strip()
373
+ chat_ids_input = request.form.get('chat_ids', '')
374
+
375
+ if not message:
376
+ flash('Message cannot be empty', 'error')
377
+ return redirect(url_for('dashboard'))
378
+
379
+ try:
380
+ if target == 'specific':
381
+ # Send to specific users
382
+ chat_ids = [int(cid.strip()) for cid in chat_ids_input.split(',') if cid.strip()]
383
+ if not chat_ids:
384
+ flash('Please provide at least one chat ID', 'error')
385
+ return redirect(url_for('dashboard'))
386
+ elif target == 'active':
387
+ # Get active users (last 7 days)
388
+ from datetime import datetime, timedelta
389
+ result = supabase.table("telegram_users").select("chat_id").gte(
390
+ "last_active",
391
+ (datetime.now() - timedelta(days=7)).isoformat()
392
+ ).execute()
393
+ chat_ids = [u['chat_id'] for u in result.data]
394
+ else:
395
+ # All users
396
+ result = supabase.table("telegram_users").select("chat_id").execute()
397
+ chat_ids = [u['chat_id'] for u in result.data]
398
+
399
+ # Send messages
400
+ success_count = 0
401
+ fail_count = 0
402
+
403
+ for chat_id in chat_ids:
404
+ if send_telegram_message(chat_id, message):
405
+ success_count += 1
406
+ else:
407
+ fail_count += 1
408
+
409
+ flash(f'Broadcast complete! Sent: {success_count}, Failed: {fail_count}', 'success')
410
+
411
+ except Exception as e:
412
+ flash(f'Error: {str(e)}', 'error')
413
+
414
+ return redirect(url_for('dashboard'))
415
+
416
+
417
+ # Import for stats
418
+ from datetime import datetime, timedelta
419
+
420
+ if __name__ == "__main__":
421
+ app.run(host="0.0.0.0", port=7860, debug=False)
bot.py CHANGED
@@ -1,682 +1,682 @@
1
- import os
2
- import logging
3
- import requests
4
- import asyncio
5
- from datetime import datetime, timezone, timedelta
6
- from dotenv import load_dotenv
7
- from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
8
- from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes, ConversationHandler
9
- from supabase import create_client, Client
10
-
11
- # Load environment variables
12
- load_dotenv()
13
-
14
- # Configuration
15
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
16
- SUPABASE_URL = os.getenv("SUPABASE_URL")
17
- SUPABASE_KEY = os.getenv("SUPABASE_KEY")
18
-
19
- # Logging setup
20
- logging.basicConfig(
21
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
22
- )
23
- logger = logging.getLogger(__name__)
24
-
25
- # Supabase Client
26
- supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
27
-
28
- # States for ConversationHandler
29
- WAITING_FOR_GEN_TYPE = 1
30
- WAITING_FOR_PROMPT = 2
31
- WAITING_FOR_SIZE = 3
32
- WAITING_FOR_IMAGE = 4
33
- WAITING_FOR_IMAGE_ACTION = 5
34
-
35
- # Coin costs for each generation type
36
- COIN_COST_TEXT_TO_IMAGE = 1
37
- COIN_COST_IMAGE_TO_IMAGE = 3
38
- COIN_COST_MULTI_IMAGE = 5
39
-
40
- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
41
- """Send a message when the command /start is issued."""
42
- user = update.effective_user
43
- chat_id = update.effective_chat.id
44
-
45
- # Register/Update user in Supabase
46
- try:
47
- data = {
48
- "p_chat_id": chat_id,
49
- "p_username": user.username,
50
- "p_first_name": user.first_name,
51
- "p_last_name": user.last_name,
52
- "p_language_code": user.language_code,
53
- "p_is_bot": user.is_bot
54
- }
55
- supabase.rpc("upsert_telegram_user", data).execute()
56
- logger.info(f"User {user.id} upserted to Supabase")
57
- except Exception as e:
58
- logger.error(f"Error upserting user: {e}")
59
-
60
- await show_main_menu(update, context)
61
-
62
-
63
- import io
64
- from http.server import HTTPServer, BaseHTTPRequestHandler
65
- from threading import Thread
66
-
67
- # Dummy Server for Hugging Face Health Check (Port 7860)
68
- class HealthCheckHandler(BaseHTTPRequestHandler):
69
- def do_GET(self):
70
- self.send_response(200)
71
- self.end_headers()
72
- self.wfile.write(b"Bot is running")
73
-
74
- def run_health_server():
75
- port = 7860
76
- server = HTTPServer(("0.0.0.0", port), HealthCheckHandler)
77
- print(f"Health check server running on port {port}")
78
- server.serve_forever()
79
-
80
- def main():
81
- """Start the bot."""
82
- # Start health check server in background
83
- Thread(target=run_health_server, daemon=True).start()
84
-
85
- if not TELEGRAM_TOKEN:
86
- print("Error: TELEGRAM_BOT_TOKEN not found.")
87
- return
88
-
89
- # Support for custom API URL (Proxy)
90
- base_url = os.getenv("TELEGRAM_API_BASE_URL")
91
- base_file_url = os.getenv("TELEGRAM_API_FILE_URL")
92
-
93
- builder = Application.builder().token(TELEGRAM_TOKEN)
94
-
95
- if base_url:
96
- builder.base_url(base_url)
97
- print(f"Using Custom API URL: {base_url}")
98
-
99
- if base_file_url:
100
- builder.base_file_url(base_file_url)
101
-
102
- application = builder.build()
103
-
104
- # Conversation handler for generating commands
105
- conv_handler = ConversationHandler(
106
- entry_points=[CallbackQueryHandler(enter_generate_mode, pattern="^generate_mode$")],
107
- states={
108
- WAITING_FOR_GEN_TYPE: [
109
- CallbackQueryHandler(handle_gen_type_selection, pattern="^gen_type_"),
110
- ],
111
- WAITING_FOR_PROMPT: [
112
- MessageHandler(filters.TEXT & ~filters.COMMAND, handle_prompt)
113
- ],
114
- WAITING_FOR_SIZE: [
115
- CallbackQueryHandler(handle_size_selection)
116
- ],
117
- WAITING_FOR_IMAGE: [
118
- MessageHandler(filters.PHOTO, handle_image_upload),
119
- CallbackQueryHandler(cancel_generation, pattern="^cancel_gen$"),
120
- ],
121
- WAITING_FOR_IMAGE_ACTION: [
122
- CallbackQueryHandler(handle_image_action, pattern="^img_action_"),
123
- CallbackQueryHandler(cancel_generation, pattern="^cancel_gen$"),
124
- ],
125
- },
126
- fallbacks=[
127
- CommandHandler("start", start),
128
- CallbackQueryHandler(show_main_menu, pattern="^main_menu$")
129
- ]
130
- )
131
-
132
- application.add_handler(CommandHandler("start", start))
133
- application.add_handler(conv_handler)
134
-
135
- # Menu callbacks
136
- application.add_handler(CallbackQueryHandler(show_main_menu, pattern="^main_menu$"))
137
- application.add_handler(CallbackQueryHandler(show_profile, pattern="^profile$"))
138
-
139
- application.add_handler(CallbackQueryHandler(show_help, pattern="^help$"))
140
-
141
- # Run the bot
142
- print("Bot is running...")
143
- application.run_polling()
144
-
145
- async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
146
- """Displays the main menu with inline buttons."""
147
- user = update.effective_user
148
- # Use @username if available, otherwise fallback to first_name
149
- if user.username:
150
- username = f"@{user.username}"
151
- else:
152
- username = user.first_name if user.first_name else "User"
153
-
154
- # Clear any stored generation data
155
- context.user_data.clear()
156
-
157
- keyboard = [
158
- [InlineKeyboardButton("Generate Image", callback_data="generate_mode")],
159
- [InlineKeyboardButton("My Profile", callback_data="profile")],
160
- [InlineKeyboardButton("Help & Support", callback_data="help")]
161
- ]
162
- reply_markup = InlineKeyboardMarkup(keyboard)
163
-
164
- text = (
165
- f"Hi {username}, welcome to the @iceboxai\\_bot.\n\n"
166
- "✨ **What you can create:**\n"
167
- "• Realistic photos\n"
168
- "• Anime & illustration\n"
169
- "• Cinematic portraits\n"
170
- "• Fantasy & concept art\n"
171
- "• Logos & product visuals\n\n"
172
- "Choose an option below to get started 👇\n\n"
173
- )
174
-
175
- if update.callback_query:
176
- await update.callback_query.answer()
177
- try:
178
- await update.callback_query.edit_message_text(text=text, reply_markup=reply_markup, parse_mode="Markdown")
179
- except:
180
- await update.callback_query.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
181
- else:
182
- await update.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
183
-
184
- return ConversationHandler.END
185
-
186
-
187
- async def get_user_coins_info(chat_id: int):
188
- """Get user coin balance and reset time."""
189
- try:
190
- user_response = supabase.table("telegram_users").select("id, tier, token_balance, daily_images_generated").eq("chat_id", chat_id).execute()
191
- if not user_response.data:
192
- return None, 0, None
193
-
194
- user = user_response.data[0]
195
- user_uuid = user['id']
196
-
197
- # Get limits via RPC
198
- check = supabase.rpc("can_generate_image", {"p_user_id": user_uuid}).execute()
199
- status = check.data[0]
200
-
201
- # Calculate coins remaining (for free tier, use daily_remaining)
202
- if user['tier'] == 'free':
203
- coins = status['daily_remaining']
204
- else:
205
- coins = user['token_balance']
206
-
207
- # Calculate next reset time (00:00 UTC next day)
208
- now_utc = datetime.now(timezone.utc)
209
- today_reset = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
210
- if now_utc >= today_reset:
211
- next_reset = today_reset + timedelta(days=1)
212
- else:
213
- next_reset = today_reset
214
-
215
- return user_uuid, coins, next_reset
216
-
217
- except Exception as e:
218
- logger.error(f"Error getting user coins: {e}")
219
- return None, 0, None
220
-
221
-
222
- async def enter_generate_mode(update: Update, context: ContextTypes.DEFAULT_TYPE):
223
- """Show generation type selection with coin info."""
224
- query = update.callback_query
225
- await query.answer()
226
-
227
- chat_id = update.effective_chat.id
228
- user_uuid, coins, next_reset = await get_user_coins_info(chat_id)
229
-
230
- if user_uuid is None:
231
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
232
- await query.edit_message_text(
233
- text="Error: User record not found. Type /start to reset.",
234
- reply_markup=InlineKeyboardMarkup(keyboard)
235
- )
236
- return ConversationHandler.END
237
-
238
- # Store user_uuid for later use
239
- context.user_data['user_uuid'] = user_uuid
240
- context.user_data['coins'] = coins
241
-
242
- reset_time_str = next_reset.strftime('%Y-%m-%d %H:%M') + " UTC" if next_reset else "N/A"
243
-
244
- keyboard = [
245
- [InlineKeyboardButton("Text to Image", callback_data="gen_type_text")],
246
- [InlineKeyboardButton("Image to Image", callback_data="gen_type_image")],
247
- [InlineKeyboardButton("Back to Menu", callback_data="main_menu")]
248
- ]
249
- reply_markup = InlineKeyboardMarkup(keyboard)
250
-
251
- text = (
252
- f"You have 🪙 **{coins} coins** remaining\n\n"
253
- "**Choose what you want to generate:**\n\n"
254
- "```\n"
255
- f"Text to Image : {COIN_COST_TEXT_TO_IMAGE} coin\n"
256
- f"Image to Image : {COIN_COST_IMAGE_TO_IMAGE} coins\n"
257
- f"Multi Image to Image : {COIN_COST_MULTI_IMAGE} coins\n"
258
- "```\n\n"
259
- f"Your coins will be reset back to 30 at:\n"
260
- f"`{reset_time_str}`"
261
- )
262
-
263
- await query.edit_message_text(
264
- text=text,
265
- reply_markup=reply_markup,
266
- parse_mode="Markdown"
267
- )
268
- return WAITING_FOR_GEN_TYPE
269
-
270
-
271
- async def handle_gen_type_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
272
- """Handle generation type selection."""
273
- query = update.callback_query
274
- await query.answer()
275
-
276
- gen_type = query.data.replace("gen_type_", "")
277
- context.user_data['gen_type'] = gen_type
278
-
279
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
280
- reply_markup = InlineKeyboardMarkup(keyboard)
281
-
282
- if gen_type == "text":
283
- # Text to Image - go directly to prompt input
284
- context.user_data['image_urls'] = []
285
- await query.edit_message_text(
286
- text="**Please Enter your prompt:**",
287
- reply_markup=reply_markup,
288
- parse_mode="Markdown"
289
- )
290
- return WAITING_FOR_PROMPT
291
-
292
- elif gen_type == "image":
293
- # Image to Image - ask for image upload
294
- context.user_data['image_urls'] = []
295
- keyboard = [[InlineKeyboardButton("Cancel", callback_data="cancel_gen")]]
296
- reply_markup = InlineKeyboardMarkup(keyboard)
297
-
298
- await query.edit_message_text(
299
- text="**Please Upload your image:**",
300
- reply_markup=reply_markup,
301
- parse_mode="Markdown"
302
- )
303
- return WAITING_FOR_IMAGE
304
-
305
- return ConversationHandler.END
306
-
307
-
308
- async def handle_image_upload(update: Update, context: ContextTypes.DEFAULT_TYPE):
309
- """Handle image upload from user."""
310
- # Get the largest photo (best quality)
311
- photo = update.message.photo[-1]
312
- file_id = photo.file_id
313
-
314
- try:
315
- # Get file path from Telegram
316
- file = await context.bot.get_file(file_id)
317
- file_url = file.file_path
318
-
319
- # Store the image URL
320
- if 'image_urls' not in context.user_data:
321
- context.user_data['image_urls'] = []
322
-
323
- context.user_data['image_urls'].append(file_url)
324
-
325
- image_count = len(context.user_data['image_urls'])
326
-
327
- # Determine coin cost based on image count
328
- if image_count == 1:
329
- coin_cost = COIN_COST_IMAGE_TO_IMAGE
330
- cost_text = f"({COIN_COST_IMAGE_TO_IMAGE} coins)"
331
- else:
332
- coin_cost = COIN_COST_MULTI_IMAGE
333
- cost_text = f"({COIN_COST_MULTI_IMAGE} coins)"
334
-
335
- # Show action options
336
- keyboard = [
337
- [InlineKeyboardButton(f"Enter Prompt", callback_data="img_action_prompt")],
338
- [InlineKeyboardButton("Upload another Image", callback_data="img_action_upload")],
339
- [InlineKeyboardButton("❌ Cancel", callback_data="cancel_gen")]
340
- ]
341
- reply_markup = InlineKeyboardMarkup(keyboard)
342
-
343
- await update.message.reply_text(
344
- text=f"✅ Image received! ({image_count} image{'s' if image_count > 1 else ''} uploaded)\n\n"
345
- f"Choose what to do next:",
346
- reply_markup=reply_markup,
347
- parse_mode="Markdown"
348
- )
349
- return WAITING_FOR_IMAGE_ACTION
350
-
351
- except Exception as e:
352
- logger.error(f"Error processing image: {e}")
353
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
354
- await update.message.reply_text(
355
- text=f"Error processing image: {str(e)}",
356
- reply_markup=InlineKeyboardMarkup(keyboard)
357
- )
358
- return ConversationHandler.END
359
-
360
-
361
- async def handle_image_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
362
- """Handle image action selection (prompt or upload more)."""
363
- query = update.callback_query
364
- await query.answer()
365
-
366
- action = query.data.replace("img_action_", "")
367
-
368
- if action == "prompt":
369
- # User wants to enter prompt
370
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
371
- reply_markup = InlineKeyboardMarkup(keyboard)
372
-
373
- image_count = len(context.user_data.get('image_urls', []))
374
-
375
- await query.edit_message_text(
376
- text=f"**Please enter your prompt:**",
377
- reply_markup=reply_markup,
378
- parse_mode="Markdown"
379
- )
380
- return WAITING_FOR_PROMPT
381
-
382
- elif action == "upload":
383
- # User wants to upload another image
384
- keyboard = [[InlineKeyboardButton("Cancel", callback_data="cancel_gen")]]
385
- reply_markup = InlineKeyboardMarkup(keyboard)
386
-
387
- image_count = len(context.user_data.get('image_urls', []))
388
-
389
- await query.edit_message_text(
390
- text=f"**Multi Image to Image** ({image_count} image{'s' if image_count > 1 else ''} uploaded)\n\n"
391
- f"Please upload another image:",
392
- reply_markup=reply_markup,
393
- parse_mode="Markdown"
394
- )
395
- return WAITING_FOR_IMAGE
396
-
397
- return ConversationHandler.END
398
-
399
-
400
- async def cancel_generation(update: Update, context: ContextTypes.DEFAULT_TYPE):
401
- """Cancel generation and return to main menu."""
402
- query = update.callback_query
403
- await query.answer()
404
- context.user_data.clear()
405
- await show_main_menu(update, context)
406
- return ConversationHandler.END
407
-
408
-
409
- async def handle_prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
410
- """Process the image prompt and ask for size."""
411
- prompt = update.message.text
412
-
413
- if prompt.lower() in ['/start', 'cancel']:
414
- await start(update, context)
415
- return ConversationHandler.END
416
-
417
- # Save prompt to context
418
- context.user_data['prompt'] = prompt
419
-
420
- # Show Size Options
421
- keyboard = [
422
- [InlineKeyboardButton("Square (1:1)", callback_data="size_1024x1024"), InlineKeyboardButton("Portrait (3:4)", callback_data="size_1024x1280")],
423
- [InlineKeyboardButton("Landscape (4:3)", callback_data="size_1280x1024"), InlineKeyboardButton("Wide (16:9)", callback_data="size_1280x720")],
424
- [InlineKeyboardButton("Back to Home", callback_data="main_menu")]
425
- ]
426
- reply_markup = InlineKeyboardMarkup(keyboard)
427
-
428
- await update.message.reply_text(
429
- "**Select Aspect Ratio**\n\nChoose the size for your image:",
430
- reply_markup=reply_markup,
431
- parse_mode="Markdown"
432
- )
433
- return WAITING_FOR_SIZE
434
-
435
-
436
- async def handle_size_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
437
- """Execute generation with selected size."""
438
- query = update.callback_query
439
- await query.answer()
440
-
441
- data = query.data
442
-
443
- if data == "main_menu":
444
- await show_main_menu(update, context)
445
- return ConversationHandler.END
446
-
447
- # Parse size "size_WxH"
448
- try:
449
- size_str = data.split("_")[1]
450
- width, height = size_str.split("x")
451
- except:
452
- width, height = "1024", "1280" # Fallback
453
-
454
- prompt = context.user_data.get('prompt', '')
455
- chat_id = update.effective_chat.id
456
- gen_type = context.user_data.get('gen_type', 'text')
457
- image_urls = context.user_data.get('image_urls', [])
458
-
459
- # Determine model and coin cost based on generation type
460
- if gen_type == 'text':
461
- model = "zimage"
462
- coin_cost = COIN_COST_TEXT_TO_IMAGE
463
- image_param = ""
464
- elif len(image_urls) == 1:
465
- model = "gptimage"
466
- coin_cost = COIN_COST_IMAGE_TO_IMAGE
467
- image_param = f"&image={image_urls[0]}"
468
- else:
469
- model = "seedream"
470
- coin_cost = COIN_COST_MULTI_IMAGE
471
- image_param = f"&image={','.join(image_urls)}"
472
-
473
- status_msg = await query.edit_message_text(f"Processing request for `{prompt}`...", parse_mode="Markdown")
474
-
475
- try:
476
- # Get user UUID
477
- user_response = supabase.table("telegram_users").select("id").eq("chat_id", chat_id).execute()
478
- if not user_response.data:
479
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
480
- await status_msg.edit_text(
481
- "Error: User record not found. Type /start to reset.",
482
- reply_markup=InlineKeyboardMarkup(keyboard)
483
- )
484
- return ConversationHandler.END
485
-
486
- user_uuid = user_response.data[0]['id']
487
-
488
- # Check limits with required coins
489
- check = supabase.rpc("can_generate_image", {"p_user_id": user_uuid, "p_coins_required": coin_cost}).execute()
490
- result = check.data[0]
491
-
492
- if not result['can_generate']:
493
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
494
- await status_msg.edit_text(
495
- f"Request Denied.\nReason: {result['reason']}",
496
- reply_markup=InlineKeyboardMarkup(keyboard)
497
- )
498
- return ConversationHandler.END
499
-
500
- # Generate
501
- gen_type_display = "Text to Image" if gen_type == 'text' else f"Image to Image ({len(image_urls)} image{'s' if len(image_urls) > 1 else ''})"
502
- await status_msg.edit_text(f"Generating image ({size_str})...\nType: {gen_type_display}\nPlease wait...", parse_mode="Markdown")
503
-
504
- api_key = os.getenv("POLLINATIONS_KEY", "")
505
- seed = int(datetime.now().timestamp() * 1000) % 100000
506
- image_url = f"https://gen.pollinations.ai/image/{prompt}?model={model}&width={width}&height={height}&seed={seed}{image_param}"
507
- if api_key:
508
- image_url += f"&key={api_key}"
509
-
510
- response = requests.get(image_url)
511
- if response.status_code == 200:
512
- # Process & Deduct
513
- log_data = {
514
- "p_user_id": user_uuid,
515
- "p_chat_id": chat_id,
516
- "p_prompt": prompt,
517
- "p_model_used": model,
518
- "p_image_size": size_str,
519
- "p_seed": seed,
520
- "p_generation_time_ms": int(response.elapsed.total_seconds() * 1000),
521
- "p_coins_used": coin_cost
522
- }
523
- # RPC handles deduction and returns new stats
524
- log_res = supabase.rpc("process_image_generation", log_data).execute()
525
- log_info = log_res.data[0]
526
-
527
- # Delete status message so clean chat
528
- await status_msg.delete()
529
-
530
- # 1. Send Image (Clean)
531
- await context.bot.send_photo(
532
- chat_id=chat_id,
533
- photo=io.BytesIO(response.content)
534
- )
535
-
536
- # 2. Send Separate Info/Menu Message
537
- if log_info['tier_used'] == 'free':
538
- recheck = supabase.rpc("can_generate_image", {"p_user_id": user_uuid}).execute()
539
- rem = recheck.data[0]
540
- limit_status = f"`{rem['daily_remaining']}` coins remaining today"
541
-
542
- # Calculate next reset time (00:00 UTC)
543
- now_utc = datetime.now(timezone.utc)
544
- today_reset = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
545
- if now_utc >= today_reset:
546
- next_reset = today_reset + timedelta(days=1)
547
- else:
548
- next_reset = today_reset
549
- reset_time = f"{next_reset.strftime('%Y-%m-%d %H:%M')} UTC"
550
- else:
551
- limit_status = f"`{log_info['tokens_remaining']}` coins remaining"
552
- reset_time = "Never (Paid)"
553
-
554
- info_text = (
555
- f"✅ **Generation Complete!**\n\n"
556
- f"**Coins Used:** {coin_cost}\n"
557
- f"**Balance:** {limit_status}\n\n"
558
- f"**Coins will reset at:**\n"
559
- f"`{reset_time}`"
560
- )
561
-
562
- keyboard = [
563
- [InlineKeyboardButton("Generate Again", callback_data="generate_mode")],
564
- [InlineKeyboardButton("Back to Home", callback_data="main_menu")]
565
- ]
566
-
567
- await context.bot.send_message(
568
- chat_id=chat_id,
569
- text=info_text,
570
- reply_markup=InlineKeyboardMarkup(keyboard),
571
- parse_mode="Markdown"
572
- )
573
-
574
- else:
575
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
576
- await status_msg.edit_text(
577
- "Generation failed due to provider error.",
578
- reply_markup=InlineKeyboardMarkup(keyboard)
579
- )
580
-
581
- except Exception as e:
582
- logger.error(f"Error generation: {e}")
583
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
584
- await status_msg.edit_text(
585
- f"System error: {str(e)}",
586
- reply_markup=InlineKeyboardMarkup(keyboard)
587
- )
588
-
589
- return ConversationHandler.END
590
-
591
- async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE):
592
- """Show detailed user profile."""
593
- query = update.callback_query
594
- await query.answer()
595
-
596
- chat_id = update.effective_chat.id
597
-
598
- try:
599
- # Get basic user data
600
- res = supabase.table("telegram_users").select("*").eq("chat_id", chat_id).execute()
601
- if not res.data:
602
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
603
- await query.edit_message_text(
604
- "Profile not found.",
605
- reply_markup=InlineKeyboardMarkup(keyboard)
606
- )
607
- return
608
-
609
- u = res.data[0]
610
- user_uuid = u['id']
611
-
612
- # Get detailed limits via RPC
613
- check = supabase.rpc("can_generate_image", {"p_user_id": user_uuid}).execute()
614
- status = check.data[0]
615
-
616
- # Determine Status Text
617
- tier_display = "Free" if u.get('tier') == 'free' else "Premium"
618
- status_display = u.get('status', 'active').capitalize()
619
-
620
- # Calculate limits
621
- daily_used = u.get('daily_images_generated', 0)
622
- daily_rem = status['daily_remaining']
623
- daily_total = daily_used + daily_rem
624
-
625
- # Calculate next reset time (00:00 UTC)
626
- now_utc = datetime.now(timezone.utc)
627
- today_reset = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
628
- if now_utc >= today_reset:
629
- next_reset = today_reset + timedelta(days=1)
630
- else:
631
- next_reset = today_reset
632
- reset_display = next_reset.strftime('%Y-%m-%d %H:%M') + " UTC"
633
- total_gen = u.get('total_images_generated', 0)
634
-
635
- # Monospace format
636
- text = (
637
- f"```\n"
638
- f"Name : {u.get('first_name', 'User')}\n\n"
639
- f"ID : {chat_id}\n"
640
- f"Region : {u.get('language_code', 'en')}\n"
641
- f"Tier : {tier_display}\n"
642
- f"Status : {status_display}\n\n"
643
- f"Activity\n"
644
- f"Total Generated : {total_gen}\n\n"
645
- f"Usage\n"
646
- )
647
-
648
- if u.get('tier') == 'paid':
649
- text += f"Balance : {u.get('token_balance', 0)} coins\n"
650
- else:
651
- text += f"Daily Coins : {daily_used} / {daily_total}\n"
652
- text += f"Reset Time : {reset_display}\n"
653
-
654
- text += "```"
655
-
656
- keyboard = [[InlineKeyboardButton("🔙 Back to Menu", callback_data="main_menu")]]
657
- await query.edit_message_text(text=text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
658
-
659
- except Exception as e:
660
- logger.error(f"Profile error: {e}")
661
- keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
662
- await query.edit_message_text(
663
- "Could not load profile.",
664
- reply_markup=InlineKeyboardMarkup(keyboard)
665
- )
666
-
667
- async def show_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
668
- """Show help and contact."""
669
- query = update.callback_query
670
- await query.answer()
671
-
672
- text = (
673
- "**Help & Support**\n\n"
674
- "If you encounter any issues or have questions, please contact our admin:\n"
675
- "📩 Contact: @pinturusak\n"
676
- )
677
-
678
- keyboard = [[InlineKeyboardButton("Back to Menu", callback_data="main_menu")]]
679
- await query.edit_message_text(text=text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
680
-
681
- if __name__ == "__main__":
682
- main()
 
1
+ import os
2
+ import logging
3
+ import requests
4
+ import asyncio
5
+ from datetime import datetime, timezone, timedelta
6
+ from dotenv import load_dotenv
7
+ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
8
+ from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ContextTypes, ConversationHandler
9
+ from supabase import create_client, Client
10
+
11
+ # Load environment variables
12
+ load_dotenv()
13
+
14
+ # Configuration
15
+ TELEGRAM_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
16
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
17
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
18
+
19
+ # Logging setup
20
+ logging.basicConfig(
21
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Supabase Client
26
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
27
+
28
+ # States for ConversationHandler
29
+ WAITING_FOR_GEN_TYPE = 1
30
+ WAITING_FOR_PROMPT = 2
31
+ WAITING_FOR_SIZE = 3
32
+ WAITING_FOR_IMAGE = 4
33
+ WAITING_FOR_IMAGE_ACTION = 5
34
+
35
+ # Coin costs for each generation type
36
+ COIN_COST_TEXT_TO_IMAGE = 1
37
+ COIN_COST_IMAGE_TO_IMAGE = 3
38
+ COIN_COST_MULTI_IMAGE = 5
39
+
40
+ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
41
+ """Send a message when the command /start is issued."""
42
+ user = update.effective_user
43
+ chat_id = update.effective_chat.id
44
+
45
+ # Register/Update user in Supabase
46
+ try:
47
+ data = {
48
+ "p_chat_id": chat_id,
49
+ "p_username": user.username,
50
+ "p_first_name": user.first_name,
51
+ "p_last_name": user.last_name,
52
+ "p_language_code": user.language_code,
53
+ "p_is_bot": user.is_bot
54
+ }
55
+ supabase.rpc("upsert_telegram_user", data).execute()
56
+ logger.info(f"User {user.id} upserted to Supabase")
57
+ except Exception as e:
58
+ logger.error(f"Error upserting user: {e}")
59
+
60
+ await show_main_menu(update, context)
61
+
62
+
63
+ import io
64
+ from http.server import HTTPServer, BaseHTTPRequestHandler
65
+ from threading import Thread
66
+
67
+ # Dummy Server for Hugging Face Health Check (Port 7860)
68
+ class HealthCheckHandler(BaseHTTPRequestHandler):
69
+ def do_GET(self):
70
+ self.send_response(200)
71
+ self.end_headers()
72
+ self.wfile.write(b"Bot is running")
73
+
74
+ def run_health_server():
75
+ port = 7860
76
+ server = HTTPServer(("0.0.0.0", port), HealthCheckHandler)
77
+ print(f"Health check server running on port {port}")
78
+ server.serve_forever()
79
+
80
+ def main():
81
+ """Start the bot."""
82
+ # Start health check server in background
83
+ Thread(target=run_health_server, daemon=True).start()
84
+
85
+ if not TELEGRAM_TOKEN:
86
+ print("Error: TELEGRAM_BOT_TOKEN not found.")
87
+ return
88
+
89
+ # Support for custom API URL (Proxy)
90
+ base_url = os.getenv("TELEGRAM_API_BASE_URL")
91
+ base_file_url = os.getenv("TELEGRAM_API_FILE_URL")
92
+
93
+ builder = Application.builder().token(TELEGRAM_TOKEN)
94
+
95
+ if base_url:
96
+ builder.base_url(base_url)
97
+ print(f"Using Custom API URL: {base_url}")
98
+
99
+ if base_file_url:
100
+ builder.base_file_url(base_file_url)
101
+
102
+ application = builder.build()
103
+
104
+ # Conversation handler for generating commands
105
+ conv_handler = ConversationHandler(
106
+ entry_points=[CallbackQueryHandler(enter_generate_mode, pattern="^generate_mode$")],
107
+ states={
108
+ WAITING_FOR_GEN_TYPE: [
109
+ CallbackQueryHandler(handle_gen_type_selection, pattern="^gen_type_"),
110
+ ],
111
+ WAITING_FOR_PROMPT: [
112
+ MessageHandler(filters.TEXT & ~filters.COMMAND, handle_prompt)
113
+ ],
114
+ WAITING_FOR_SIZE: [
115
+ CallbackQueryHandler(handle_size_selection)
116
+ ],
117
+ WAITING_FOR_IMAGE: [
118
+ MessageHandler(filters.PHOTO, handle_image_upload),
119
+ CallbackQueryHandler(cancel_generation, pattern="^cancel_gen$"),
120
+ ],
121
+ WAITING_FOR_IMAGE_ACTION: [
122
+ CallbackQueryHandler(handle_image_action, pattern="^img_action_"),
123
+ CallbackQueryHandler(cancel_generation, pattern="^cancel_gen$"),
124
+ ],
125
+ },
126
+ fallbacks=[
127
+ CommandHandler("start", start),
128
+ CallbackQueryHandler(show_main_menu, pattern="^main_menu$")
129
+ ]
130
+ )
131
+
132
+ application.add_handler(CommandHandler("start", start))
133
+ application.add_handler(conv_handler)
134
+
135
+ # Menu callbacks
136
+ application.add_handler(CallbackQueryHandler(show_main_menu, pattern="^main_menu$"))
137
+ application.add_handler(CallbackQueryHandler(show_profile, pattern="^profile$"))
138
+
139
+ application.add_handler(CallbackQueryHandler(show_help, pattern="^help$"))
140
+
141
+ # Run the bot
142
+ print("Bot is running...")
143
+ application.run_polling()
144
+
145
+ async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
146
+ """Displays the main menu with inline buttons."""
147
+ user = update.effective_user
148
+ # Use @username if available, otherwise fallback to first_name
149
+ if user.username:
150
+ username = f"@{user.username}"
151
+ else:
152
+ username = user.first_name if user.first_name else "User"
153
+
154
+ # Clear any stored generation data
155
+ context.user_data.clear()
156
+
157
+ keyboard = [
158
+ [InlineKeyboardButton("Generate Image", callback_data="generate_mode")],
159
+ [InlineKeyboardButton("My Profile", callback_data="profile")],
160
+ [InlineKeyboardButton("Help & Support", callback_data="help")]
161
+ ]
162
+ reply_markup = InlineKeyboardMarkup(keyboard)
163
+
164
+ text = (
165
+ f"Hi {username}, welcome to the @iceboxai\\_bot.\n\n"
166
+ "✨ **What you can create:**\n"
167
+ "• Realistic photos\n"
168
+ "• Anime & illustration\n"
169
+ "• Cinematic portraits\n"
170
+ "• Fantasy & concept art\n"
171
+ "• Logos & product visuals\n\n"
172
+ "Choose an option below to get started 👇\n\n"
173
+ )
174
+
175
+ if update.callback_query:
176
+ await update.callback_query.answer()
177
+ try:
178
+ await update.callback_query.edit_message_text(text=text, reply_markup=reply_markup, parse_mode="Markdown")
179
+ except:
180
+ await update.callback_query.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
181
+ else:
182
+ await update.message.reply_text(text, reply_markup=reply_markup, parse_mode="Markdown")
183
+
184
+ return ConversationHandler.END
185
+
186
+
187
+ async def get_user_coins_info(chat_id: int):
188
+ """Get user coin balance and reset time."""
189
+ try:
190
+ user_response = supabase.table("telegram_users").select("id, tier, token_balance, daily_images_generated").eq("chat_id", chat_id).execute()
191
+ if not user_response.data:
192
+ return None, 0, None
193
+
194
+ user = user_response.data[0]
195
+ user_uuid = user['id']
196
+
197
+ # Get limits via RPC
198
+ check = supabase.rpc("can_generate_image", {"p_user_id": user_uuid}).execute()
199
+ status = check.data[0]
200
+
201
+ # Calculate coins remaining (for free tier, use daily_remaining)
202
+ if user['tier'] == 'free':
203
+ coins = status['daily_remaining']
204
+ else:
205
+ coins = user['token_balance']
206
+
207
+ # Calculate next reset time (00:00 UTC next day)
208
+ now_utc = datetime.now(timezone.utc)
209
+ today_reset = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
210
+ if now_utc >= today_reset:
211
+ next_reset = today_reset + timedelta(days=1)
212
+ else:
213
+ next_reset = today_reset
214
+
215
+ return user_uuid, coins, next_reset
216
+
217
+ except Exception as e:
218
+ logger.error(f"Error getting user coins: {e}")
219
+ return None, 0, None
220
+
221
+
222
+ async def enter_generate_mode(update: Update, context: ContextTypes.DEFAULT_TYPE):
223
+ """Show generation type selection with coin info."""
224
+ query = update.callback_query
225
+ await query.answer()
226
+
227
+ chat_id = update.effective_chat.id
228
+ user_uuid, coins, next_reset = await get_user_coins_info(chat_id)
229
+
230
+ if user_uuid is None:
231
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
232
+ await query.edit_message_text(
233
+ text="Error: User record not found. Type /start to reset.",
234
+ reply_markup=InlineKeyboardMarkup(keyboard)
235
+ )
236
+ return ConversationHandler.END
237
+
238
+ # Store user_uuid for later use
239
+ context.user_data['user_uuid'] = user_uuid
240
+ context.user_data['coins'] = coins
241
+
242
+ reset_time_str = next_reset.strftime('%Y-%m-%d %H:%M') + " UTC" if next_reset else "N/A"
243
+
244
+ keyboard = [
245
+ [InlineKeyboardButton("Text to Image", callback_data="gen_type_text")],
246
+ [InlineKeyboardButton("Image to Image", callback_data="gen_type_image")],
247
+ [InlineKeyboardButton("Back to Menu", callback_data="main_menu")]
248
+ ]
249
+ reply_markup = InlineKeyboardMarkup(keyboard)
250
+
251
+ text = (
252
+ f"You have 🪙 **{coins} coins** remaining\n\n"
253
+ "**Choose what you want to generate:**\n\n"
254
+ "```\n"
255
+ f"Text to Image : {COIN_COST_TEXT_TO_IMAGE} coin\n"
256
+ f"Image to Image : {COIN_COST_IMAGE_TO_IMAGE} coins\n"
257
+ f"Multi Image to Image : {COIN_COST_MULTI_IMAGE} coins\n"
258
+ "```\n"
259
+ f"Your coins will be reset back to 30 at:\n"
260
+ f"`{reset_time_str}`"
261
+ )
262
+
263
+ await query.edit_message_text(
264
+ text=text,
265
+ reply_markup=reply_markup,
266
+ parse_mode="Markdown"
267
+ )
268
+ return WAITING_FOR_GEN_TYPE
269
+
270
+
271
+ async def handle_gen_type_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
272
+ """Handle generation type selection."""
273
+ query = update.callback_query
274
+ await query.answer()
275
+
276
+ gen_type = query.data.replace("gen_type_", "")
277
+ context.user_data['gen_type'] = gen_type
278
+
279
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
280
+ reply_markup = InlineKeyboardMarkup(keyboard)
281
+
282
+ if gen_type == "text":
283
+ # Text to Image - go directly to prompt input
284
+ context.user_data['image_urls'] = []
285
+ await query.edit_message_text(
286
+ text="**Please Enter your prompt:**",
287
+ reply_markup=reply_markup,
288
+ parse_mode="Markdown"
289
+ )
290
+ return WAITING_FOR_PROMPT
291
+
292
+ elif gen_type == "image":
293
+ # Image to Image - ask for image upload
294
+ context.user_data['image_urls'] = []
295
+ keyboard = [[InlineKeyboardButton("Cancel", callback_data="cancel_gen")]]
296
+ reply_markup = InlineKeyboardMarkup(keyboard)
297
+
298
+ await query.edit_message_text(
299
+ text="**Please Upload your image:**",
300
+ reply_markup=reply_markup,
301
+ parse_mode="Markdown"
302
+ )
303
+ return WAITING_FOR_IMAGE
304
+
305
+ return ConversationHandler.END
306
+
307
+
308
+ async def handle_image_upload(update: Update, context: ContextTypes.DEFAULT_TYPE):
309
+ """Handle image upload from user."""
310
+ # Get the largest photo (best quality)
311
+ photo = update.message.photo[-1]
312
+ file_id = photo.file_id
313
+
314
+ try:
315
+ # Get file path from Telegram
316
+ file = await context.bot.get_file(file_id)
317
+ file_url = file.file_path
318
+
319
+ # Store the image URL
320
+ if 'image_urls' not in context.user_data:
321
+ context.user_data['image_urls'] = []
322
+
323
+ context.user_data['image_urls'].append(file_url)
324
+
325
+ image_count = len(context.user_data['image_urls'])
326
+
327
+ # Determine coin cost based on image count
328
+ if image_count == 1:
329
+ coin_cost = COIN_COST_IMAGE_TO_IMAGE
330
+ cost_text = f"({COIN_COST_IMAGE_TO_IMAGE} coins)"
331
+ else:
332
+ coin_cost = COIN_COST_MULTI_IMAGE
333
+ cost_text = f"({COIN_COST_MULTI_IMAGE} coins)"
334
+
335
+ # Show action options
336
+ keyboard = [
337
+ [InlineKeyboardButton(f"Enter Prompt", callback_data="img_action_prompt")],
338
+ [InlineKeyboardButton("Upload another Image", callback_data="img_action_upload")],
339
+ [InlineKeyboardButton("❌ Cancel", callback_data="cancel_gen")]
340
+ ]
341
+ reply_markup = InlineKeyboardMarkup(keyboard)
342
+
343
+ await update.message.reply_text(
344
+ text=f"✅ Image received! ({image_count} image{'s' if image_count > 1 else ''} uploaded)\n\n"
345
+ f"Choose what to do next:",
346
+ reply_markup=reply_markup,
347
+ parse_mode="Markdown"
348
+ )
349
+ return WAITING_FOR_IMAGE_ACTION
350
+
351
+ except Exception as e:
352
+ logger.error(f"Error processing image: {e}")
353
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
354
+ await update.message.reply_text(
355
+ text=f"Error processing image: {str(e)}",
356
+ reply_markup=InlineKeyboardMarkup(keyboard)
357
+ )
358
+ return ConversationHandler.END
359
+
360
+
361
+ async def handle_image_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
362
+ """Handle image action selection (prompt or upload more)."""
363
+ query = update.callback_query
364
+ await query.answer()
365
+
366
+ action = query.data.replace("img_action_", "")
367
+
368
+ if action == "prompt":
369
+ # User wants to enter prompt
370
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
371
+ reply_markup = InlineKeyboardMarkup(keyboard)
372
+
373
+ image_count = len(context.user_data.get('image_urls', []))
374
+
375
+ await query.edit_message_text(
376
+ text=f"**Please enter your prompt:**",
377
+ reply_markup=reply_markup,
378
+ parse_mode="Markdown"
379
+ )
380
+ return WAITING_FOR_PROMPT
381
+
382
+ elif action == "upload":
383
+ # User wants to upload another image
384
+ keyboard = [[InlineKeyboardButton("Cancel", callback_data="cancel_gen")]]
385
+ reply_markup = InlineKeyboardMarkup(keyboard)
386
+
387
+ image_count = len(context.user_data.get('image_urls', []))
388
+
389
+ await query.edit_message_text(
390
+ text=f"**Multi Image to Image** ({image_count} image{'s' if image_count > 1 else ''} uploaded)\n\n"
391
+ f"Please upload another image:",
392
+ reply_markup=reply_markup,
393
+ parse_mode="Markdown"
394
+ )
395
+ return WAITING_FOR_IMAGE
396
+
397
+ return ConversationHandler.END
398
+
399
+
400
+ async def cancel_generation(update: Update, context: ContextTypes.DEFAULT_TYPE):
401
+ """Cancel generation and return to main menu."""
402
+ query = update.callback_query
403
+ await query.answer()
404
+ context.user_data.clear()
405
+ await show_main_menu(update, context)
406
+ return ConversationHandler.END
407
+
408
+
409
+ async def handle_prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
410
+ """Process the image prompt and ask for size."""
411
+ prompt = update.message.text
412
+
413
+ if prompt.lower() in ['/start', 'cancel']:
414
+ await start(update, context)
415
+ return ConversationHandler.END
416
+
417
+ # Save prompt to context
418
+ context.user_data['prompt'] = prompt
419
+
420
+ # Show Size Options
421
+ keyboard = [
422
+ [InlineKeyboardButton("Square (1:1)", callback_data="size_1024x1024"), InlineKeyboardButton("Portrait (3:4)", callback_data="size_1024x1280")],
423
+ [InlineKeyboardButton("Landscape (4:3)", callback_data="size_1280x1024"), InlineKeyboardButton("Wide (16:9)", callback_data="size_1280x720")],
424
+ [InlineKeyboardButton("Back to Home", callback_data="main_menu")]
425
+ ]
426
+ reply_markup = InlineKeyboardMarkup(keyboard)
427
+
428
+ await update.message.reply_text(
429
+ "**Select Aspect Ratio**\n\nChoose the size for your image:",
430
+ reply_markup=reply_markup,
431
+ parse_mode="Markdown"
432
+ )
433
+ return WAITING_FOR_SIZE
434
+
435
+
436
+ async def handle_size_selection(update: Update, context: ContextTypes.DEFAULT_TYPE):
437
+ """Execute generation with selected size."""
438
+ query = update.callback_query
439
+ await query.answer()
440
+
441
+ data = query.data
442
+
443
+ if data == "main_menu":
444
+ await show_main_menu(update, context)
445
+ return ConversationHandler.END
446
+
447
+ # Parse size "size_WxH"
448
+ try:
449
+ size_str = data.split("_")[1]
450
+ width, height = size_str.split("x")
451
+ except:
452
+ width, height = "1024", "1280" # Fallback
453
+
454
+ prompt = context.user_data.get('prompt', '')
455
+ chat_id = update.effective_chat.id
456
+ gen_type = context.user_data.get('gen_type', 'text')
457
+ image_urls = context.user_data.get('image_urls', [])
458
+
459
+ # Determine model and coin cost based on generation type
460
+ if gen_type == 'text':
461
+ model = "zimage"
462
+ coin_cost = COIN_COST_TEXT_TO_IMAGE
463
+ image_param = ""
464
+ elif len(image_urls) == 1:
465
+ model = "gptimage"
466
+ coin_cost = COIN_COST_IMAGE_TO_IMAGE
467
+ image_param = f"&image={image_urls[0]}"
468
+ else:
469
+ model = "seedream"
470
+ coin_cost = COIN_COST_MULTI_IMAGE
471
+ image_param = f"&image={','.join(image_urls)}"
472
+
473
+ status_msg = await query.edit_message_text(f"Processing request for `{prompt}`...", parse_mode="Markdown")
474
+
475
+ try:
476
+ # Get user UUID
477
+ user_response = supabase.table("telegram_users").select("id").eq("chat_id", chat_id).execute()
478
+ if not user_response.data:
479
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
480
+ await status_msg.edit_text(
481
+ "Error: User record not found. Type /start to reset.",
482
+ reply_markup=InlineKeyboardMarkup(keyboard)
483
+ )
484
+ return ConversationHandler.END
485
+
486
+ user_uuid = user_response.data[0]['id']
487
+
488
+ # Check limits with required coins
489
+ check = supabase.rpc("can_generate_image", {"p_user_id": user_uuid, "p_coins_required": coin_cost}).execute()
490
+ result = check.data[0]
491
+
492
+ if not result['can_generate']:
493
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
494
+ await status_msg.edit_text(
495
+ f"Request Denied.\nReason: {result['reason']}",
496
+ reply_markup=InlineKeyboardMarkup(keyboard)
497
+ )
498
+ return ConversationHandler.END
499
+
500
+ # Generate
501
+ gen_type_display = "Text to Image" if gen_type == 'text' else f"Image to Image ({len(image_urls)} image{'s' if len(image_urls) > 1 else ''})"
502
+ await status_msg.edit_text(f"Generating image ({size_str})...\nType: {gen_type_display}\nPlease wait...", parse_mode="Markdown")
503
+
504
+ api_key = os.getenv("POLLINATIONS_KEY", "")
505
+ seed = int(datetime.now().timestamp() * 1000) % 100000
506
+ image_url = f"https://gen.pollinations.ai/image/{prompt}?model={model}&width={width}&height={height}&seed={seed}{image_param}"
507
+ if api_key:
508
+ image_url += f"&key={api_key}"
509
+
510
+ response = requests.get(image_url)
511
+ if response.status_code == 200:
512
+ # Process & Deduct
513
+ log_data = {
514
+ "p_user_id": user_uuid,
515
+ "p_chat_id": chat_id,
516
+ "p_prompt": prompt,
517
+ "p_model_used": model,
518
+ "p_image_size": size_str,
519
+ "p_seed": seed,
520
+ "p_generation_time_ms": int(response.elapsed.total_seconds() * 1000),
521
+ "p_coins_used": coin_cost
522
+ }
523
+ # RPC handles deduction and returns new stats
524
+ log_res = supabase.rpc("process_image_generation", log_data).execute()
525
+ log_info = log_res.data[0]
526
+
527
+ # Delete status message so clean chat
528
+ await status_msg.delete()
529
+
530
+ # 1. Send Image (Clean)
531
+ await context.bot.send_photo(
532
+ chat_id=chat_id,
533
+ photo=io.BytesIO(response.content)
534
+ )
535
+
536
+ # 2. Send Separate Info/Menu Message
537
+ if log_info['tier_used'] == 'free':
538
+ recheck = supabase.rpc("can_generate_image", {"p_user_id": user_uuid}).execute()
539
+ rem = recheck.data[0]
540
+ limit_status = f"`{rem['daily_remaining']}` coins remaining today"
541
+
542
+ # Calculate next reset time (00:00 UTC)
543
+ now_utc = datetime.now(timezone.utc)
544
+ today_reset = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
545
+ if now_utc >= today_reset:
546
+ next_reset = today_reset + timedelta(days=1)
547
+ else:
548
+ next_reset = today_reset
549
+ reset_time = f"{next_reset.strftime('%Y-%m-%d %H:%M')} UTC"
550
+ else:
551
+ limit_status = f"`{log_info['tokens_remaining']}` coins remaining"
552
+ reset_time = "Never (Paid)"
553
+
554
+ info_text = (
555
+ f"✅ **Generation Complete!**\n\n"
556
+ f"**Coins Used:** {coin_cost}\n"
557
+ f"**Balance:** {limit_status}\n\n"
558
+ f"**Coins will reset at:**\n"
559
+ f"`{reset_time}`"
560
+ )
561
+
562
+ keyboard = [
563
+ [InlineKeyboardButton("Generate Again", callback_data="generate_mode")],
564
+ [InlineKeyboardButton("Back to Home", callback_data="main_menu")]
565
+ ]
566
+
567
+ await context.bot.send_message(
568
+ chat_id=chat_id,
569
+ text=info_text,
570
+ reply_markup=InlineKeyboardMarkup(keyboard),
571
+ parse_mode="Markdown"
572
+ )
573
+
574
+ else:
575
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
576
+ await status_msg.edit_text(
577
+ "Generation failed due to provider error.",
578
+ reply_markup=InlineKeyboardMarkup(keyboard)
579
+ )
580
+
581
+ except Exception as e:
582
+ logger.error(f"Error generation: {e}")
583
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
584
+ await status_msg.edit_text(
585
+ f"System error: {str(e)}",
586
+ reply_markup=InlineKeyboardMarkup(keyboard)
587
+ )
588
+
589
+ return ConversationHandler.END
590
+
591
+ async def show_profile(update: Update, context: ContextTypes.DEFAULT_TYPE):
592
+ """Show detailed user profile."""
593
+ query = update.callback_query
594
+ await query.answer()
595
+
596
+ chat_id = update.effective_chat.id
597
+
598
+ try:
599
+ # Get basic user data
600
+ res = supabase.table("telegram_users").select("*").eq("chat_id", chat_id).execute()
601
+ if not res.data:
602
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
603
+ await query.edit_message_text(
604
+ "Profile not found.",
605
+ reply_markup=InlineKeyboardMarkup(keyboard)
606
+ )
607
+ return
608
+
609
+ u = res.data[0]
610
+ user_uuid = u['id']
611
+
612
+ # Get detailed limits via RPC
613
+ check = supabase.rpc("can_generate_image", {"p_user_id": user_uuid}).execute()
614
+ status = check.data[0]
615
+
616
+ # Determine Status Text
617
+ tier_display = "Free" if u.get('tier') == 'free' else "Premium"
618
+ status_display = u.get('status', 'active').capitalize()
619
+
620
+ # Calculate limits
621
+ daily_used = u.get('daily_images_generated', 0)
622
+ daily_rem = status['daily_remaining']
623
+ daily_total = daily_used + daily_rem
624
+
625
+ # Calculate next reset time (00:00 UTC)
626
+ now_utc = datetime.now(timezone.utc)
627
+ today_reset = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
628
+ if now_utc >= today_reset:
629
+ next_reset = today_reset + timedelta(days=1)
630
+ else:
631
+ next_reset = today_reset
632
+ reset_display = next_reset.strftime('%Y-%m-%d %H:%M') + " UTC"
633
+ total_gen = u.get('total_images_generated', 0)
634
+
635
+ # Monospace format
636
+ text = (
637
+ f"```\n"
638
+ f"Name : {u.get('first_name', 'User')}\n\n"
639
+ f"ID : {chat_id}\n"
640
+ f"Region : {u.get('language_code', 'en')}\n"
641
+ f"Tier : {tier_display}\n"
642
+ f"Status : {status_display}\n\n"
643
+ f"Activity\n"
644
+ f"Total Generated : {total_gen}\n\n"
645
+ f"Usage\n"
646
+ )
647
+
648
+ if u.get('tier') == 'paid':
649
+ text += f"Balance : {u.get('token_balance', 0)} coins\n"
650
+ else:
651
+ text += f"Daily Coins : {daily_used} / {daily_total}\n"
652
+ text += f"Reset Time : {reset_display}\n"
653
+
654
+ text += "```"
655
+
656
+ keyboard = [[InlineKeyboardButton("🔙 Back to Menu", callback_data="main_menu")]]
657
+ await query.edit_message_text(text=text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
658
+
659
+ except Exception as e:
660
+ logger.error(f"Profile error: {e}")
661
+ keyboard = [[InlineKeyboardButton("Back to Home", callback_data="main_menu")]]
662
+ await query.edit_message_text(
663
+ "Could not load profile.",
664
+ reply_markup=InlineKeyboardMarkup(keyboard)
665
+ )
666
+
667
+ async def show_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
668
+ """Show help and contact."""
669
+ query = update.callback_query
670
+ await query.answer()
671
+
672
+ text = (
673
+ "**Help & Support**\n\n"
674
+ "If you encounter any issues or have questions, please contact our admin:\n"
675
+ "📩 Contact: @pinturusak\n"
676
+ )
677
+
678
+ keyboard = [[InlineKeyboardButton("Back to Menu", callback_data="main_menu")]]
679
+ await query.edit_message_text(text=text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
680
+
681
+ if __name__ == "__main__":
682
+ main()
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
- python-telegram-bot>=20.7
2
- supabase
3
- requests
4
- python-dotenv
 
 
1
+ python-telegram-bot>=20.7
2
+ supabase
3
+ requests
4
+ python-dotenv
5
+ flask
start.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Start both the Telegram bot and Admin console
3
+
4
+ # Start the admin web console in background
5
+ python admin.py &
6
+
7
+ # Start the Telegram bot (main process)
8
+ python bot.py