Aleksmorshen commited on
Commit
54a6ad1
·
verified ·
1 Parent(s): 60ab832

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -150
app.py CHANGED
@@ -2,10 +2,13 @@ from flask import Flask, Response, request, jsonify
2
  import requests
3
  import json
4
  import os
 
 
5
 
6
  app = Flask(__name__)
7
 
8
  HOTSPOTS_FILE = 'hotspots.json'
 
9
 
10
  def get_all_hotspots():
11
  if not os.path.exists(HOTSPOTS_FILE):
@@ -30,53 +33,47 @@ def index():
30
  <head>
31
  <meta charset="UTF-8">
32
  <title>TON AR Hotspots</title>
33
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
34
- <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
35
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
36
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
37
  <style>
 
 
 
 
 
 
38
  body, html {
39
  margin: 0;
40
  padding: 0;
41
  width: 100%;
42
  height: 100%;
43
  overflow: hidden;
44
- background-color: #000;
45
- color: white;
46
- font-family: Arial, sans-serif;
 
 
 
 
 
47
  }
48
- #login-wall {
49
  position: fixed;
50
  top: 0;
51
  left: 0;
52
  width: 100%;
53
  height: 100%;
54
  background-color: rgba(17, 17, 17, 0.95);
55
- z-index: 100;
56
  display: flex;
57
  flex-direction: column;
58
  align-items: center;
59
  justify-content: center;
60
- transition: opacity 0.5s ease;
61
- }
62
- #login-wall.hidden {
63
- opacity: 0;
64
- pointer-events: none;
65
- }
66
- #login-content {
67
  text-align: center;
68
- }
69
- h1 {
70
- margin-bottom: 20px;
71
- }
72
- #ton-connect-button {
73
- margin-top: 10px;
74
- }
75
- #status, #balance {
76
- margin-top: 15px;
77
- font-size: 16px;
78
- word-break: break-all;
79
- padding: 0 10px;
80
  }
81
  #ar-container {
82
  position: absolute;
@@ -176,30 +173,23 @@ def index():
176
  </style>
177
  </head>
178
  <body>
179
- <div id="login-wall">
180
- <div id="login-content">
181
- <h1>AR Хотспоты на TON</h1>
182
- <p>Для входа подключите свой кошелек</p>
183
- <div id="ton-connect-button"></div>
184
- <div id="status">Статус: не подключено</div>
185
- <div id="balance">Баланс: —</div>
186
- </div>
187
  </div>
188
 
189
- <video id="camera-view" playsinline autoplay muted></video>
190
- <div id="ar-container"></div>
 
191
 
192
- <div id="map-container">
193
- <div id="map"></div>
194
- <button id="toggle-map-button">Minimap</button>
 
195
  </div>
196
 
197
  <script>
198
- const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
199
- manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
200
- buttonRootId: 'ton-connect-button'
201
- });
202
-
203
  const state = {
204
  hotspots: [],
205
  currentUserPosition: null,
@@ -208,24 +198,28 @@ def index():
208
  map: null,
209
  userMarker: null,
210
  hotspotMarkers: [],
211
- initialMapSet: false
 
212
  };
 
213
 
214
- const MAX_VISIBLE_DISTANCE = 10;
215
-
216
- async function fetchBalance(address) {
217
- try {
218
- const res = await fetch(`/get_balance?address=${address}`);
219
- const data = await res.json();
220
- document.getElementById('balance').innerText = data.balance !== undefined ?
221
- `Баланс: ${data.balance} TON` : `Баланс: ошибка`;
222
- } catch (e) {
223
- document.getElementById('balance').innerText = `Баланс: ошибка`;
224
  }
 
 
 
 
 
 
 
 
 
225
  }
226
 
227
  async function initArApp() {
228
- document.getElementById('login-wall').classList.add('hidden');
229
  await setupCamera();
230
  setupGPS();
231
  setupOrientationListener();
@@ -235,20 +229,6 @@ def index():
235
  requestAnimationFrame(update);
236
  }
237
 
238
- tonConnectUI.onStatusChange(wallet => {
239
- const statusDiv = document.getElementById('status');
240
- if (wallet) {
241
- const address = wallet.account.address;
242
- statusDiv.innerHTML = `✅ Подключено:<br>${address.slice(0, 6)}...${address.slice(-4)}`;
243
- fetchBalance(address);
244
- initArApp();
245
- } else {
246
- statusDiv.innerText = '❌ Не подключено';
247
- document.getElementById('balance').innerText = `Баланс: —`;
248
- document.getElementById('login-wall').classList.remove('hidden');
249
- }
250
- });
251
-
252
  async function setupCamera() {
253
  const video = document.getElementById('camera-view');
254
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
@@ -275,7 +255,7 @@ def index():
275
  if (state.userMarker) {
276
  state.userMarker.setLatLng([state.currentUserPosition.lat, state.currentUserPosition.lon]);
277
  if (!state.initialMapSet) {
278
- state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], 15);
279
  state.initialMapSet = true;
280
  }
281
  }
@@ -289,34 +269,47 @@ def index():
289
  alert('GPS не поддерживается вашим браузером.');
290
  }
291
  }
292
-
293
  function setupOrientationListener() {
294
- if (window.DeviceOrientationEvent) {
295
- window.addEventListener('deviceorientation', (event) => {
296
- if (event.alpha !== null) {
297
- state.deviceOrientation.alpha = event.alpha;
298
- state.deviceOrientation.beta = event.beta;
299
- state.deviceOrientation.gamma = event.gamma;
300
- }
301
- }, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  } else {
303
  alert('Отслеживание ориентации устройства не поддерживается.');
304
  }
305
  }
306
-
307
  function initMap() {
308
  state.map = L.map('map').setView([0, 0], 2);
309
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
310
  attribution: '© OpenStreetMap contributors'
311
  }).addTo(state.map);
312
  state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Ваше местоположение').openPopup();
313
-
314
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
315
  }
316
 
317
  function toggleMap() {
318
  const mapContainer = document.getElementById('map-container');
319
  mapContainer.classList.toggle('fullscreen');
 
320
  setTimeout(() => {
321
  state.map.invalidateSize();
322
  if (state.currentUserPosition) {
@@ -344,68 +337,81 @@ def index():
344
  const el = document.createElement('div');
345
  el.className = 'hotspot';
346
  el.id = `hotspot-${index}`;
347
- el.innerHTML = `${hotspot.text}<br><small>by ${hotspot.creator_address ? hotspot.creator_address.slice(0, 6) + '...' + hotspot.creator_address.slice(-4) : 'Unknown'}</small>`;
348
  container.appendChild(el);
349
  });
350
  }
351
 
352
  function renderHotspotsOnMap() {
353
  if (!state.map) return;
354
-
355
  state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
356
  state.hotspotMarkers = [];
357
-
358
  state.hotspots.forEach(hotspot => {
359
  const marker = L.marker([hotspot.lat, hotspot.lon])
360
  .addTo(state.map)
361
- .bindPopup(`${hotspot.text}<br>by ${hotspot.creator_address ? hotspot.creator_address.slice(0, 6) + '...' + hotspot.creator_address.slice(-4) : 'Unknown'}`);
362
  state.hotspotMarkers.push(marker);
363
  });
364
  }
365
 
366
  function setupAddHotspotListener() {
367
  const container = document.getElementById('ar-container');
368
- container.addEventListener('click', async (event) => {
 
 
369
  if (!state.currentUserPosition) {
370
- alert('GPS-координаты еще не определены. Подождите немного.');
371
- return;
372
- }
373
- if (!tonConnectUI.account || !tonConnectUI.account.address) {
374
- alert('Для добавления хотспота необходимо подключить кошелек.');
375
  return;
376
  }
377
 
378
- const text = prompt('Введите текст для нового хотспота:');
379
- if (text) {
380
- const newHotspot = {
381
- text: text,
382
- lat: state.currentUserPosition.lat,
383
- lon: state.currentUserPosition.lon,
384
- creator_address: tonConnectUI.account.address
385
- };
386
-
387
- try {
388
- const response = await fetch('/hotspots', {
389
- method: 'POST',
390
- headers: { 'Content-Type': 'application/json' },
391
- body: JSON.stringify(newHotspot)
392
- });
393
- if(response.ok) {
394
- const savedHotspot = await response.json();
395
- state.hotspots.push(savedHotspot.hotspot);
396
- renderHotspots();
397
- renderHotspotsOnMap();
398
- } else {
399
- alert('Не удалось сохранить хотспот.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  }
401
- } catch (error) {
402
- console.error('Ошибка сохранения хотспота:', error);
403
- alert('Ошибка сети при сохранении хотспота.');
404
  }
405
- }
 
 
 
406
  });
407
  }
408
-
409
  function haversineDistance(coords1, coords2) {
410
  function toRad(x) { return x * Math.PI / 180; }
411
  const R = 6371;
@@ -437,69 +443,57 @@ def index():
437
  requestAnimationFrame(update);
438
  return;
439
  }
440
-
441
  const screenWidth = window.innerWidth;
442
  const screenHeight = window.innerHeight;
443
- const halfFovRad = (state.cameraFov / 2) * (Math.PI / 180);
444
-
445
  state.hotspots.forEach((hotspot, index) => {
446
  const el = document.getElementById(`hotspot-${index}`);
447
  if (!el) return;
448
-
449
  const distance = haversineDistance(state.currentUserPosition, hotspot);
450
-
451
  if (distance > MAX_VISIBLE_DISTANCE) {
452
  el.classList.add('hidden');
453
  return;
454
  }
455
-
456
  const bearing = calculateBearing(state.currentUserPosition, hotspot);
457
  let angleDiff = bearing - state.deviceOrientation.alpha;
458
-
459
  if (angleDiff > 180) angleDiff -= 360;
460
  if (angleDiff < -180) angleDiff += 360;
461
-
462
  if (Math.abs(angleDiff) > state.cameraFov / 2) {
463
  el.classList.add('hidden');
464
  } else {
465
  el.classList.remove('hidden');
466
-
467
  const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
468
  const y = screenHeight / 2;
469
-
470
  const scale = Math.max(0.5, 1 - distance / MAX_VISIBLE_DISTANCE);
471
-
472
  el.style.left = `${x}px`;
473
  el.style.top = `${y}px`;
474
  el.style.transform = `translate(-50%, -50%) scale(${scale})`;
475
  el.style.zIndex = Math.round(1000 - distance);
476
  }
477
  });
478
-
479
  requestAnimationFrame(update);
480
  }
 
 
481
  </script>
482
  </body>
483
  </html>
484
  '''
485
  return Response(html_content, mimetype='text/html')
486
 
487
- @app.route('/get_balance')
488
- def get_balance():
489
- address = request.args.get('address')
490
- if not address:
491
- return jsonify({'error': 'No address provided'}), 400
492
  try:
493
- res = requests.get(f'https://tonapi.io/v2/accounts/{address}')
494
- res.raise_for_status()
495
- data = res.json()
496
- raw_balance = int(data.get('balance', 0))
497
- ton_balance = raw_balance / 1e9
498
- return jsonify({'balance': round(ton_balance, 4)})
499
- except requests.exceptions.RequestException as e:
500
- return jsonify({'error': f'Failed to fetch from tonapi: {e}'}), 500
501
- except (ValueError, KeyError):
502
- return jsonify({'error': 'Invalid data received from tonapi'}), 500
 
 
503
 
504
  @app.route('/hotspots', methods=['GET', 'POST'])
505
  def handle_hotspots():
@@ -507,6 +501,10 @@ def handle_hotspots():
507
  return jsonify(get_all_hotspots())
508
 
509
  if request.method == 'POST':
 
 
 
 
510
  if not request.is_json:
511
  return jsonify({"error": "Missing JSON in request"}), 400
512
 
@@ -514,17 +512,19 @@ def handle_hotspots():
514
  text = data.get('text')
515
  lat = data.get('lat')
516
  lon = data.get('lon')
517
- creator_address = data.get('creator_address')
 
518
 
519
- if not all([text, lat, lon, creator_address]):
520
- return jsonify({"error": "Missing data: text, lat, lon, or creator_address"}), 400
521
 
522
  try:
523
  new_hotspot = {
524
  "text": str(text),
525
  "lat": float(lat),
526
  "lon": float(lon),
527
- "creator_address": str(creator_address)
 
528
  }
529
  save_hotspot(new_hotspot)
530
  return jsonify({"success": True, "hotspot": new_hotspot}), 201
 
2
  import requests
3
  import json
4
  import os
5
+ import hmac
6
+ import hashlib
7
 
8
  app = Flask(__name__)
9
 
10
  HOTSPOTS_FILE = 'hotspots.json'
11
+ BOT_TOKEN = '6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo'
12
 
13
  def get_all_hotspots():
14
  if not os.path.exists(HOTSPOTS_FILE):
 
33
  <head>
34
  <meta charset="UTF-8">
35
  <title>TON AR Hotspots</title>
36
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
37
+ <script src="https://telegram.org/js/telegram-web-app.js"></script>
38
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
39
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
40
  <style>
41
+ :root {
42
+ --tg-theme-bg-color: #000000;
43
+ --tg-theme-text-color: #ffffff;
44
+ --tg-theme-button-color: #007aff;
45
+ --tg-theme-button-text-color: #ffffff;
46
+ }
47
  body, html {
48
  margin: 0;
49
  padding: 0;
50
  width: 100%;
51
  height: 100%;
52
  overflow: hidden;
53
+ background-color: var(--tg-theme-bg-color);
54
+ color: var(--tg-theme-text-color);
55
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
56
+ }
57
+ #app-container {
58
+ width: 100%;
59
+ height: 100%;
60
+ display: none;
61
  }
62
+ #error-wall {
63
  position: fixed;
64
  top: 0;
65
  left: 0;
66
  width: 100%;
67
  height: 100%;
68
  background-color: rgba(17, 17, 17, 0.95);
69
+ z-index: 200;
70
  display: flex;
71
  flex-direction: column;
72
  align-items: center;
73
  justify-content: center;
 
 
 
 
 
 
 
74
  text-align: center;
75
+ padding: 20px;
76
+ box-sizing: border-box;
 
 
 
 
 
 
 
 
 
 
77
  }
78
  #ar-container {
79
  position: absolute;
 
173
  </style>
174
  </head>
175
  <body>
176
+ <div id="error-wall">
177
+ <h1>Ошибка</h1>
178
+ <p>Это приложение предназначено для запуска внутри Telegram. Пожалуйста, откройте его через вашего Telegram-бота.</p>
 
 
 
 
 
179
  </div>
180
 
181
+ <div id="app-container">
182
+ <video id="camera-view" playsinline autoplay muted></video>
183
+ <div id="ar-container"></div>
184
 
185
+ <div id="map-container">
186
+ <div id="map"></div>
187
+ <button id="toggle-map-button">Minimap</button>
188
+ </div>
189
  </div>
190
 
191
  <script>
192
+ const tg = window.Telegram.WebApp;
 
 
 
 
193
  const state = {
194
  hotspots: [],
195
  currentUserPosition: null,
 
198
  map: null,
199
  userMarker: null,
200
  hotspotMarkers: [],
201
+ initialMapSet: false,
202
+ telegramUser: null
203
  };
204
+ const MAX_VISIBLE_DISTANCE = 25;
205
 
206
+ async function initApp() {
207
+ if (!tg.initData || !tg.initDataUnsafe.user) {
208
+ document.getElementById('error-wall').style.display = 'flex';
209
+ return;
 
 
 
 
 
 
210
  }
211
+
212
+ tg.ready();
213
+ tg.expand();
214
+
215
+ state.telegramUser = tg.initDataUnsafe.user;
216
+ document.getElementById('error-wall').style.display = 'none';
217
+ document.getElementById('app-container').style.display = 'block';
218
+
219
+ await initArApp();
220
  }
221
 
222
  async function initArApp() {
 
223
  await setupCamera();
224
  setupGPS();
225
  setupOrientationListener();
 
229
  requestAnimationFrame(update);
230
  }
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  async function setupCamera() {
233
  const video = document.getElementById('camera-view');
234
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
 
255
  if (state.userMarker) {
256
  state.userMarker.setLatLng([state.currentUserPosition.lat, state.currentUserPosition.lon]);
257
  if (!state.initialMapSet) {
258
+ state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], 16);
259
  state.initialMapSet = true;
260
  }
261
  }
 
269
  alert('GPS не поддерживается вашим браузером.');
270
  }
271
  }
272
+
273
  function setupOrientationListener() {
274
+ const handleOrientation = (event) => {
275
+ if (event.alpha !== null) {
276
+ state.deviceOrientation.alpha = event.alpha;
277
+ state.deviceOrientation.beta = event.beta;
278
+ state.deviceOrientation.gamma = event.gamma;
279
+ }
280
+ };
281
+ if (window.DeviceOrientationEvent && typeof window.DeviceOrientationEvent.requestPermission === 'function') {
282
+ window.DeviceOrientationEvent.requestPermission()
283
+ .then(permissionState => {
284
+ if (permissionState === 'granted') {
285
+ window.addEventListener('deviceorientationabsolute', handleOrientation, true);
286
+ } else {
287
+ alert('Доступ к ориентации устройства отклонен.');
288
+ }
289
+ })
290
+ .catch(console.error);
291
+ } else if ('ondeviceorientationabsolute' in window) {
292
+ window.addEventListener('deviceorientationabsolute', handleOrientation, true);
293
+ } else if ('ondeviceorientation' in window) {
294
+ window.addEventListener('deviceorientation', handleOrientation, true);
295
  } else {
296
  alert('Отслеживание ориентации устройства не поддерживается.');
297
  }
298
  }
299
+
300
  function initMap() {
301
  state.map = L.map('map').setView([0, 0], 2);
302
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
303
  attribution: '© OpenStreetMap contributors'
304
  }).addTo(state.map);
305
  state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Ваше местоположение').openPopup();
 
306
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
307
  }
308
 
309
  function toggleMap() {
310
  const mapContainer = document.getElementById('map-container');
311
  mapContainer.classList.toggle('fullscreen');
312
+ tg.HapticFeedback.impactOccurred('light');
313
  setTimeout(() => {
314
  state.map.invalidateSize();
315
  if (state.currentUserPosition) {
 
337
  const el = document.createElement('div');
338
  el.className = 'hotspot';
339
  el.id = `hotspot-${index}`;
340
+ el.innerHTML = `${hotspot.text}<br><small>by @${hotspot.creator_username || 'Unknown'}</small>`;
341
  container.appendChild(el);
342
  });
343
  }
344
 
345
  function renderHotspotsOnMap() {
346
  if (!state.map) return;
 
347
  state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
348
  state.hotspotMarkers = [];
 
349
  state.hotspots.forEach(hotspot => {
350
  const marker = L.marker([hotspot.lat, hotspot.lon])
351
  .addTo(state.map)
352
+ .bindPopup(`${hotspot.text}<br>by @${hotspot.creator_username || 'Unknown'}`);
353
  state.hotspotMarkers.push(marker);
354
  });
355
  }
356
 
357
  function setupAddHotspotListener() {
358
  const container = document.getElementById('ar-container');
359
+ container.addEventListener('click', (event) => {
360
+ if (event.target !== container) return;
361
+
362
  if (!state.currentUserPosition) {
363
+ tg.showAlert('GPS-координаты еще не определены. Подождите немного.');
 
 
 
 
364
  return;
365
  }
366
 
367
+ tg.showPopup({
368
+ title: 'Новый хотспот',
369
+ message: 'Введите текст для нового хотспота:',
370
+ buttons: [{ type: 'ok', text: 'Сохранить' }, { type: 'cancel' }]
371
+ }, async (buttonId) => {
372
+ if (buttonId === 'ok') {
373
+ tg.HapticFeedback.impactOccurred('heavy');
374
+ const text = tg.PopupButton.text;
375
+ if (text) {
376
+ const newHotspot = {
377
+ text: text,
378
+ lat: state.currentUserPosition.lat,
379
+ lon: state.currentUserPosition.lon,
380
+ creator_id: state.telegramUser.id,
381
+ creator_username: state.telegramUser.username || 'user' + state.telegramUser.id
382
+ };
383
+
384
+ try {
385
+ const response = await fetch('/hotspots', {
386
+ method: 'POST',
387
+ headers: {
388
+ 'Content-Type': 'application/json',
389
+ 'X-Telegram-Init-Data': tg.initData
390
+ },
391
+ body: JSON.stringify(newHotspot)
392
+ });
393
+ if(response.ok) {
394
+ const savedHotspot = await response.json();
395
+ state.hotspots.push(savedHotspot.hotspot);
396
+ renderHotspots();
397
+ renderHotspotsOnMap();
398
+ } else {
399
+ const errorData = await response.json();
400
+ tg.showAlert('Не удалось сохранить хотспот: ' + (errorData.error || 'Server error'));
401
+ }
402
+ } catch (error) {
403
+ console.error('Ошибка сохранения хотспота:', error);
404
+ tg.showAlert('Ошибка сети при сохранении хотспота.');
405
+ }
406
  }
 
 
 
407
  }
408
+ });
409
+
410
+ tg.MainButton.setText('Введите текст выше и нажмите "Сохранить"').show();
411
+ tg.on('popupClosed', () => { tg.MainButton.hide(); });
412
  });
413
  }
414
+
415
  function haversineDistance(coords1, coords2) {
416
  function toRad(x) { return x * Math.PI / 180; }
417
  const R = 6371;
 
443
  requestAnimationFrame(update);
444
  return;
445
  }
 
446
  const screenWidth = window.innerWidth;
447
  const screenHeight = window.innerHeight;
 
 
448
  state.hotspots.forEach((hotspot, index) => {
449
  const el = document.getElementById(`hotspot-${index}`);
450
  if (!el) return;
 
451
  const distance = haversineDistance(state.currentUserPosition, hotspot);
 
452
  if (distance > MAX_VISIBLE_DISTANCE) {
453
  el.classList.add('hidden');
454
  return;
455
  }
 
456
  const bearing = calculateBearing(state.currentUserPosition, hotspot);
457
  let angleDiff = bearing - state.deviceOrientation.alpha;
 
458
  if (angleDiff > 180) angleDiff -= 360;
459
  if (angleDiff < -180) angleDiff += 360;
 
460
  if (Math.abs(angleDiff) > state.cameraFov / 2) {
461
  el.classList.add('hidden');
462
  } else {
463
  el.classList.remove('hidden');
 
464
  const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
465
  const y = screenHeight / 2;
 
466
  const scale = Math.max(0.5, 1 - distance / MAX_VISIBLE_DISTANCE);
 
467
  el.style.left = `${x}px`;
468
  el.style.top = `${y}px`;
469
  el.style.transform = `translate(-50%, -50%) scale(${scale})`;
470
  el.style.zIndex = Math.round(1000 - distance);
471
  }
472
  });
 
473
  requestAnimationFrame(update);
474
  }
475
+
476
+ document.addEventListener('DOMContentLoaded', initApp);
477
  </script>
478
  </body>
479
  </html>
480
  '''
481
  return Response(html_content, mimetype='text/html')
482
 
483
+ def is_valid_telegram_data(init_data_str: str) -> bool:
 
 
 
 
484
  try:
485
+ params = dict(p.split('=', 1) for p in init_data_str.split('&'))
486
+ hash_from_telegram = params.pop('hash')
487
+
488
+ sorted_keys = sorted(params.keys())
489
+ data_check_string = "\n".join(f"{key}={params[key]}" for key in sorted_keys)
490
+
491
+ secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
492
+ calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
493
+
494
+ return calculated_hash == hash_from_telegram
495
+ except Exception:
496
+ return False
497
 
498
  @app.route('/hotspots', methods=['GET', 'POST'])
499
  def handle_hotspots():
 
501
  return jsonify(get_all_hotspots())
502
 
503
  if request.method == 'POST':
504
+ init_data = request.headers.get('X-Telegram-Init-Data')
505
+ if not init_data or not is_valid_telegram_data(init_data):
506
+ return jsonify({"error": "Unauthorized: Invalid Telegram InitData"}), 401
507
+
508
  if not request.is_json:
509
  return jsonify({"error": "Missing JSON in request"}), 400
510
 
 
512
  text = data.get('text')
513
  lat = data.get('lat')
514
  lon = data.get('lon')
515
+ creator_id = data.get('creator_id')
516
+ creator_username = data.get('creator_username')
517
 
518
+ if not all([text, lat, lon, creator_id, creator_username]):
519
+ return jsonify({"error": "Missing data"}), 400
520
 
521
  try:
522
  new_hotspot = {
523
  "text": str(text),
524
  "lat": float(lat),
525
  "lon": float(lon),
526
+ "creator_id": int(creator_id),
527
+ "creator_username": str(creator_username)
528
  }
529
  save_hotspot(new_hotspot)
530
  return jsonify({"success": True, "hotspot": new_hotspot}), 201