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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +200 -142
app.py CHANGED
@@ -1,14 +1,15 @@
1
  from flask import Flask, Response, request, jsonify
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):
@@ -54,26 +55,16 @@ def index():
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;
@@ -107,6 +98,7 @@ def index():
107
  will-change: transform, left, top, opacity;
108
  z-index: 10;
109
  text-align: center;
 
110
  }
111
  .hotspot small {
112
  font-size: 0.7em;
@@ -173,52 +165,50 @@ def index():
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,
 
196
  deviceOrientation: { alpha: 0, beta: 0, gamma: 0 },
 
197
  cameraFov: 60,
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();
@@ -229,6 +219,30 @@ def index():
229
  requestAnimationFrame(update);
230
  }
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  async function setupCamera() {
233
  const video = document.getElementById('camera-view');
234
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
@@ -252,10 +266,18 @@ def index():
252
  lat: position.coords.latitude,
253
  lon: position.coords.longitude
254
  };
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,51 +291,56 @@ def index():
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) {
316
- state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], state.map.getZoom());
317
  }
318
  }, 300);
319
  }
@@ -337,7 +364,7 @@ def index():
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
  }
@@ -349,72 +376,73 @@ def index():
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;
418
  const dLat = toRad(coords2.lat - coords1.lat);
419
  const dLon = toRad(coords2.lon - coords1.lon);
420
  const lat1 = toRad(coords1.lat);
@@ -422,7 +450,7 @@ def index():
422
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
423
  Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
424
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
425
- return R * c * 1000;
426
  }
427
 
428
  function calculateBearing(start, end) {
@@ -439,61 +467,93 @@ def index():
439
  }
440
 
441
  function update() {
442
- if (!state.currentUserPosition) {
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,10 +561,6 @@ 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
 
@@ -515,8 +571,8 @@ def handle_hotspots():
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 = {
@@ -532,4 +588,6 @@ def handle_hotspots():
532
  return jsonify({"error": "Invalid data types"}), 400
533
 
534
  if __name__ == '__main__':
 
 
535
  app.run(host='0.0.0.0', port=7860, debug=False)
 
1
  from flask import Flask, Response, request, jsonify
 
2
  import json
3
  import os
4
  import hmac
5
  import hashlib
6
+ from urllib.parse import unquote
7
 
8
  app = Flask(__name__)
9
 
10
+ # ВАЖНО: В реальном проекте храните токен в переменных окружения, а не в коде.
11
+ BOT_TOKEN = "6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo"
12
  HOTSPOTS_FILE = 'hotspots.json'
 
13
 
14
  def get_all_hotspots():
15
  if not os.path.exists(HOTSPOTS_FILE):
 
55
  color: var(--tg-theme-text-color);
56
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
57
  }
58
+ #info-bar {
 
 
 
 
 
59
  position: fixed;
60
+ top: 10px;
61
+ left: 10px;
62
+ background-color: rgba(0, 0, 0, 0.7);
63
+ color: white;
64
+ padding: 5px 10px;
65
+ border-radius: 8px;
66
+ z-index: 51;
67
+ font-size: 14px;
 
 
 
 
 
68
  }
69
  #ar-container {
70
  position: absolute;
 
98
  will-change: transform, left, top, opacity;
99
  z-index: 10;
100
  text-align: center;
101
+ cursor: pointer;
102
  }
103
  .hotspot small {
104
  font-size: 0.7em;
 
165
  </style>
166
  </head>
167
  <body>
168
+ <div id="info-bar">Авторизация...</div>
169
+ <video id="camera-view" playsinline autoplay muted></video>
170
+ <div id="ar-container"></div>
171
+ <div id="map-container">
172
+ <div id="map"></div>
173
+ <button id="toggle-map-button">Minimap</button>
 
 
 
 
 
 
 
174
  </div>
175
 
176
  <script>
177
  const tg = window.Telegram.WebApp;
178
+
179
  const state = {
180
  hotspots: [],
181
+ currentUser: null,
182
  currentUserPosition: null,
183
+ smoothedPosition: null,
184
  deviceOrientation: { alpha: 0, beta: 0, gamma: 0 },
185
+ smoothedOrientation: { alpha: 0, beta: 0, gamma: 0 },
186
  cameraFov: 60,
187
  map: null,
188
  userMarker: null,
189
  hotspotMarkers: [],
190
+ initialMapSet: false
 
191
  };
 
 
 
 
 
 
 
 
 
 
192
 
193
+ const SMOOTHING_FACTOR_GPS = 0.05;
194
+ const SMOOTHING_FACTOR_ORIENTATION = 0.1;
195
+ const MAX_VISIBLE_DISTANCE = 1000;
196
 
197
+ async function verifyTelegramData(initData) {
198
+ try {
199
+ const response = await fetch('/verify_telegram_data', {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify({ initData: initData })
203
+ });
204
+ const data = await response.json();
205
+ return data.ok;
206
+ } catch (error) {
207
+ console.error('Verification failed:', error);
208
+ return false;
209
+ }
210
  }
211
+
212
  async function initArApp() {
213
  await setupCamera();
214
  setupGPS();
 
219
  requestAnimationFrame(update);
220
  }
221
 
222
+ tg.onEvent('viewportChanged', () => tg.expand());
223
+
224
+ window.addEventListener('load', async () => {
225
+ tg.ready();
226
+ tg.expand();
227
+
228
+ const initData = tg.initData;
229
+ const isVerified = await verifyTelegramData(initData);
230
+
231
+ if (!isVerified && !tg.initDataUnsafe.query_id) {
232
+ document.body.innerHTML = '<h1>Ошибка авторизации. Пожалуйста, откройте это приложение через Telegram.</h1>';
233
+ return;
234
+ }
235
+
236
+ state.currentUser = tg.initDataUnsafe.user;
237
+ if(state.currentUser) {
238
+ document.getElementById('info-bar').innerText = `Привет, ${state.currentUser.first_name}!`;
239
+ } else {
240
+ document.getElementById('info-bar').innerText = 'Гость';
241
+ }
242
+
243
+ initArApp();
244
+ });
245
+
246
  async function setupCamera() {
247
  const video = document.getElementById('camera-view');
248
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
 
266
  lat: position.coords.latitude,
267
  lon: position.coords.longitude
268
  };
269
+
270
+ if (!state.smoothedPosition) {
271
+ state.smoothedPosition = { ...state.currentUserPosition };
272
+ } else {
273
+ state.smoothedPosition.lat = (state.currentUserPosition.lat * SMOOTHING_FACTOR_GPS) + (state.smoothedPosition.lat * (1 - SMOOTHING_FACTOR_GPS));
274
+ state.smoothedPosition.lon = (state.currentUserPosition.lon * SMOOTHING_FACTOR_GPS) + (state.smoothedPosition.lon * (1 - SMOOTHING_FACTOR_GPS));
275
+ }
276
+
277
+ if (state.userMarker && state.smoothedPosition) {
278
+ state.userMarker.setLatLng([state.smoothedPosition.lat, state.smoothedPosition.lon]);
279
  if (!state.initialMapSet) {
280
+ state.map.setView([state.smoothedPosition.lat, state.smoothedPosition.lon], 16);
281
  state.initialMapSet = true;
282
  }
283
  }
 
291
  alert('GPS не поддерживается вашим браузером.');
292
  }
293
  }
294
+
295
  function setupOrientationListener() {
296
+ const handler = (event) => {
297
  if (event.alpha !== null) {
298
+ state.deviceOrientation.alpha = event.alpha;
299
+ state.deviceOrientation.beta = event.beta;
300
+ state.deviceOrientation.gamma = event.gamma;
301
+
302
+ let diff = state.deviceOrientation.alpha - state.smoothedOrientation.alpha;
303
+ if (diff > 180) diff -= 360;
304
+ if (diff < -180) diff += 360;
305
+
306
+ state.smoothedOrientation.alpha = (state.smoothedOrientation.alpha + diff * SMOOTHING_FACTOR_ORIENTATION + 360) % 360;
307
+ state.smoothedOrientation.beta = (state.deviceOrientation.beta * SMOOTHING_FACTOR_ORIENTATION) + (state.smoothedOrientation.beta * (1 - SMOOTHING_FACTOR_ORIENTATION));
308
+ state.smoothedOrientation.gamma = (state.deviceOrientation.gamma * SMOOTHING_FACTOR_ORIENTATION) + (state.smoothedOrientation.gamma * (1 - SMOOTHING_FACTOR_ORIENTATION));
309
  }
310
  };
311
+ if (typeof DeviceOrientationEvent.requestPermission === 'function') {
312
+ DeviceOrientationEvent.requestPermission()
313
  .then(permissionState => {
314
  if (permissionState === 'granted') {
315
+ window.addEventListener('deviceorientation', handler, true);
316
  } else {
317
+ alert('Доступ к ориентации устройства не предоставлен.');
318
  }
319
  })
320
  .catch(console.error);
321
+ } else if (window.DeviceOrientationEvent) {
322
+ window.addEventListener('deviceorientation', handler, true);
 
 
323
  } else {
324
  alert('Отслеживание ориентации устройства не поддерживается.');
325
  }
326
  }
327
+
328
  function initMap() {
329
  state.map = L.map('map').setView([0, 0], 2);
330
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
331
+ attribution: '© OpenStreetMap'
332
  }).addTo(state.map);
333
+ state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Вы здесь');
334
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
335
  }
336
 
337
  function toggleMap() {
338
  const mapContainer = document.getElementById('map-container');
339
  mapContainer.classList.toggle('fullscreen');
 
340
  setTimeout(() => {
341
  state.map.invalidateSize();
342
+ if (state.smoothedPosition) {
343
+ state.map.setView([state.smoothedPosition.lat, state.smoothedPosition.lon], state.map.getZoom());
344
  }
345
  }, 300);
346
  }
 
364
  const el = document.createElement('div');
365
  el.className = 'hotspot';
366
  el.id = `hotspot-${index}`;
367
+ el.innerHTML = `${hotspot.text}<br><small>by ${hotspot.creator_username || 'Unknown'}</small>`;
368
  container.appendChild(el);
369
  });
370
  }
 
376
  state.hotspots.forEach(hotspot => {
377
  const marker = L.marker([hotspot.lat, hotspot.lon])
378
  .addTo(state.map)
379
+ .bindPopup(`${hotspot.text}<br>by ${hotspot.creator_username || 'Unknown'}`);
380
  state.hotspotMarkers.push(marker);
381
  });
382
  }
383
 
384
  function setupAddHotspotListener() {
385
  const container = document.getElementById('ar-container');
386
+ container.addEventListener('click', async (event) => {
387
+ if (event.target.classList.contains('hotspot')) return;
388
 
389
+ if (!state.smoothedPosition) {
390
+ tg.showAlert('GPS-координаты еще не определены. Подождите.');
391
+ return;
392
+ }
393
+ if (!state.currentUser) {
394
+ tg.showAlert('Не удалось определить пользователя Telegram.');
395
  return;
396
  }
397
 
398
  tg.showPopup({
399
  title: 'Новый хотспот',
400
  message: 'Введите текст для нового хотспо��а:',
401
+ buttons: [
402
+ {id: 'save', type: 'default', text: 'Сохранить'},
403
+ {type: 'cancel'},
404
+ ]
405
  }, async (buttonId) => {
406
+ if (buttonId === 'save') {
407
+ const text = 'Новый хотспот'; // Placeholder, as popup doesn't have input
408
+ tg.showScanQrPopup({ text: "Введите текст для хотспота:" }, async (promptText) => {
409
+ if (promptText) {
410
+ const newHotspot = {
411
+ text: promptText,
412
+ lat: state.currentUserPosition.lat,
413
+ lon: state.currentUserPosition.lon,
414
+ creator_id: state.currentUser.id,
415
+ creator_username: state.currentUser.username || `${state.currentUser.first_name} ${state.currentUser.last_name || ''}`.trim()
416
+ };
417
+ try {
418
+ const response = await fetch('/hotspots', {
419
+ method: 'POST',
420
+ headers: { 'Content-Type': 'application/json' },
421
+ body: JSON.stringify(newHotspot)
422
+ });
423
+ if(response.ok) {
424
+ const savedHotspot = await response.json();
425
+ state.hotspots.push(savedHotspot.hotspot);
426
+ renderHotspots();
427
+ renderHotspotsOnMap();
428
+ tg.closeScanQrPopup();
429
+ } else {
430
+ tg.showAlert('Не удалось сохранить хотспот.');
431
+ }
432
+ } catch (error) {
433
+ tg.showAlert('Ошибка сети при сохранении хотспота.');
 
434
  }
 
 
 
435
  }
436
+ return true;
437
+ });
438
  }
439
  });
 
 
 
440
  });
441
  }
442
+
443
  function haversineDistance(coords1, coords2) {
444
  function toRad(x) { return x * Math.PI / 180; }
445
+ const R = 6371e3;
446
  const dLat = toRad(coords2.lat - coords1.lat);
447
  const dLon = toRad(coords2.lon - coords1.lon);
448
  const lat1 = toRad(coords1.lat);
 
450
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
451
  Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
452
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
453
+ return R * c;
454
  }
455
 
456
  function calculateBearing(start, end) {
 
467
  }
468
 
469
  function update() {
470
+ if (!state.smoothedPosition) {
471
  requestAnimationFrame(update);
472
  return;
473
  }
474
+
475
  const screenWidth = window.innerWidth;
476
  const screenHeight = window.innerHeight;
477
+
478
  state.hotspots.forEach((hotspot, index) => {
479
  const el = document.getElementById(`hotspot-${index}`);
480
  if (!el) return;
481
+
482
+ const distance = haversineDistance(state.smoothedPosition, hotspot);
483
+
484
  if (distance > MAX_VISIBLE_DISTANCE) {
485
  el.classList.add('hidden');
486
  return;
487
  }
488
+
489
+ const bearing = calculateBearing(state.smoothedPosition, hotspot);
490
+ let angleDiff = bearing - state.smoothedOrientation.alpha;
491
+
492
  if (angleDiff > 180) angleDiff -= 360;
493
  if (angleDiff < -180) angleDiff += 360;
494
+
495
  if (Math.abs(angleDiff) > state.cameraFov / 2) {
496
  el.classList.add('hidden');
497
  } else {
498
  el.classList.remove('hidden');
499
+
500
  const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
501
  const y = screenHeight / 2;
502
+
503
+ const scale = Math.max(0.3, 1 - (distance / MAX_VISIBLE_DISTANCE));
504
+
505
  el.style.left = `${x}px`;
506
  el.style.top = `${y}px`;
507
  el.style.transform = `translate(-50%, -50%) scale(${scale})`;
508
+ el.style.zIndex = Math.round(10000 - distance);
509
  }
510
  });
511
+
512
  requestAnimationFrame(update);
513
  }
 
 
514
  </script>
515
  </body>
516
  </html>
517
  '''
518
  return Response(html_content, mimetype='text/html')
519
 
520
+ @app.route('/verify_telegram_data', methods=['POST'])
521
+ def verify_telegram_data():
522
+ if not request.is_json:
523
+ return jsonify({"ok": False, "error": "Request must be JSON"}), 400
524
+
525
+ data = request.get_json()
526
+ init_data_str = data.get('initData')
527
+
528
+ if not init_data_str:
529
+ return jsonify({"ok": False, "error": "initData not in request"}), 400
530
+
531
  try:
532
+ unquoted_data = unquote(init_data_str)
533
+
534
+ data_check_list = []
535
+ hash_from_telegram = ''
536
+
537
+ for pair in unquoted_data.split('&'):
538
+ key, value = pair.split('=', 1)
539
+ if key == 'hash':
540
+ hash_from_telegram = value
541
+ else:
542
+ data_check_list.append(f"{key}={value}")
543
 
544
+ data_check_list.sort()
545
+ data_check_string = "\n".join(data_check_list)
546
 
547
  secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
548
  calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
549
+
550
+ if calculated_hash == hash_from_telegram:
551
+ return jsonify({"ok": True}), 200
552
+ else:
553
+ return jsonify({"ok": False, "error": "Hash validation failed"}), 403
554
+
555
+ except Exception as e:
556
+ return jsonify({"ok": False, "error": str(e)}), 500
557
 
558
  @app.route('/hotspots', methods=['GET', 'POST'])
559
  def handle_hotspots():
 
561
  return jsonify(get_all_hotspots())
562
 
563
  if request.method == 'POST':
 
 
 
 
564
  if not request.is_json:
565
  return jsonify({"error": "Missing JSON in request"}), 400
566
 
 
571
  creator_id = data.get('creator_id')
572
  creator_username = data.get('creator_username')
573
 
574
+ if not all([text, lat, lon, creator_id]):
575
+ return jsonify({"error": "Missing data: text, lat, lon, or creator_id"}), 400
576
 
577
  try:
578
  new_hotspot = {
 
588
  return jsonify({"error": "Invalid data types"}), 400
589
 
590
  if __name__ == '__main__':
591
+ # Для локального тестирования может потребоваться запуск с SSL-сертификатом
592
+ # Для развертывания на хостинге с Nginx/Caddy это не нужно
593
  app.run(host='0.0.0.0', port=7860, debug=False)