Aleksmorshen commited on
Commit
a6dadd9
·
verified ·
1 Parent(s): faff0b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +266 -214
app.py CHANGED
@@ -1,37 +1,15 @@
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
  HOTSPOTS_FILE = 'hotspots.json'
11
- BOT_TOKEN = "6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo"
12
-
13
- def validate_init_data(init_data_str, bot_token):
14
- try:
15
- data_params = sorted([
16
- (k, v) for k, v in
17
- [p.split('=', 1) for p in init_data_str.split('&')]
18
- if k != 'hash'
19
- ], key=lambda x: x[0])
20
-
21
- data_check_string = "\n".join([f"{k}={v}" for k, v in data_params])
22
-
23
- secret_key = hmac.new("WebAppData".encode(), bot_token.encode(), hashlib.sha256).digest()
24
- calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
25
-
26
- received_hash = dict(p.split('=', 1) for p in init_data_str.split('&')).get('hash')
27
-
28
- if calculated_hash == received_hash:
29
- user_data_encoded = dict(p.split('=', 1) for p in init_data_str.split('&')).get('user')
30
- if user_data_encoded:
31
- return json.loads(unquote(user_data_encoded))
32
- return None
33
- except Exception:
34
- return None
35
 
36
  def get_all_hotspots():
37
  if not os.path.exists(HOTSPOTS_FILE):
@@ -55,18 +33,19 @@ def index():
55
  <html lang="ru">
56
  <head>
57
  <meta charset="UTF-8">
58
- <title>TON AR Hotspots</title>
59
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
60
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
61
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
62
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
63
  <style>
64
  :root {
65
- --tg-bg-color: var(--tg-theme-bg-color, #000000);
66
- --tg-text-color: var(--tg-theme-text-color, #ffffff);
67
- --tg-hint-color: var(--tg-theme-hint-color, #aaaaaa);
68
  --tg-button-color: var(--tg-theme-button-color, #007aff);
69
- --tg-button-text-color: var(--tg-theme-button-text-color, #ffffff);
 
 
70
  }
71
  body, html {
72
  margin: 0;
@@ -76,22 +55,31 @@ def index():
76
  overflow: hidden;
77
  background-color: var(--tg-bg-color);
78
  color: var(--tg-text-color);
79
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
80
  }
81
- #app-container {
82
  position: fixed;
83
  top: 0;
84
  left: 0;
85
  width: 100%;
86
  height: 100%;
 
 
87
  display: flex;
88
  flex-direction: column;
89
  align-items: center;
90
  justify-content: center;
91
- }
92
- #error-wall {
93
  text-align: center;
94
- padding: 20px;
 
 
 
 
 
 
 
 
95
  }
96
  #ar-container {
97
  position: absolute;
@@ -112,25 +100,29 @@ def index():
112
  }
113
  .hotspot {
114
  position: absolute;
115
- background-color: var(--tg-button-color);
116
- color: var(--tg-button-text-color);
117
  padding: 10px 15px;
118
  border-radius: 12px;
119
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
 
120
  transform: translate(-50%, -50%);
121
  transition: opacity 0.3s, transform 0.1s linear;
122
  white-space: nowrap;
123
  font-size: 16px;
 
124
  will-change: transform, left, top, opacity;
125
  z-index: 10;
126
  text-align: center;
127
- border: 1px solid rgba(255, 255, 255, 0.2);
 
128
  }
129
  .hotspot small {
130
- font-size: 0.7em;
131
  opacity: 0.8;
132
  display: block;
133
  margin-top: 5px;
 
134
  }
135
  .hotspot.hidden {
136
  opacity: 0;
@@ -141,18 +133,17 @@ def index():
141
  bottom: 20px;
142
  left: 50%;
143
  transform: translateX(-50%);
144
- width: 90%;
145
- max-width: 400px;
146
- height: 150px;
147
- background-color: rgba(0, 0, 0, 0.7);
148
- border: 1px solid var(--tg-hint-color);
149
  border-radius: 15px;
150
  overflow: hidden;
151
- transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
152
  z-index: 50;
153
  display: flex;
154
  flex-direction: column;
155
- box-shadow: 0 5px 20px rgba(0,0,0,0.4);
 
156
  }
157
  #map-container.fullscreen {
158
  width: 100%;
@@ -162,103 +153,112 @@ def index():
162
  bottom: 0;
163
  border-radius: 0;
164
  transform: none;
165
- max-width: none;
166
- justify-content: center;
167
- align-items: center;
168
- background-color: var(--tg-bg-color);
169
  }
170
  #map {
171
  flex-grow: 1;
172
  width: 100%;
173
  height: 100%;
174
- border-radius: 15px;
175
- overflow: hidden;
176
  }
177
- #map-container.fullscreen #map {
178
- border-radius: 0;
 
 
 
179
  }
180
  #toggle-map-button {
181
- position: absolute;
182
- top: 8px;
183
- right: 8px;
184
- background-color: rgba(0, 0, 0, 0.6);
185
- color: white;
186
  border: none;
187
- padding: 6px 12px;
188
- border-radius: 20px;
189
  cursor: pointer;
190
  font-size: 14px;
191
- z-index: 51;
192
- }
193
- .leaflet-popup-content-wrapper, .leaflet-popup-tip {
194
- background-color: var(--tg-bg-color);
195
- color: var(--tg-text-color);
196
- box-shadow: 0 3px 14px rgba(0,0,0,0.4);
197
- }
198
- .leaflet-container a.leaflet-popup-close-button {
199
- color: var(--tg-text-color);
200
  }
201
  </style>
202
  </head>
203
  <body>
204
- <div id="app-container" style="display: none;">
 
 
 
 
 
205
  <video id="camera-view" playsinline autoplay muted></video>
206
  <div id="ar-container"></div>
207
  <div id="map-container">
208
  <div id="map"></div>
209
- <button id="toggle-map-button">Карта</button>
 
 
210
  </div>
211
  </div>
212
 
213
- <div id="error-wall">
214
- <h1>Загрузка...</h1>
215
- <p>Это приложение должно быть открыто в Telegram.</p>
216
- </div>
217
-
218
  <script>
219
- const tg = window.Telegram.WebApp;
220
-
221
  const state = {
222
  hotspots: [],
 
223
  currentUserPosition: null,
224
- deviceOrientation: { alpha: 0, beta: 0, gamma: 0, smoothedAlpha: 0 },
 
225
  cameraFov: 60,
226
  map: null,
227
  userMarker: null,
228
  hotspotMarkers: [],
229
  initialMapSet: false,
230
- telegramUser: null
231
  };
232
 
233
- const MAX_VISIBLE_DISTANCE = 10000; // 10 km
234
- const ORIENTATION_SMOOTHING = 0.1;
235
 
236
- function initArApp() {
237
- document.getElementById('error-wall').style.display = 'none';
238
- document.getElementById('app-container').style.display = 'block';
239
- setupCamera();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  setupGPS();
241
  setupOrientationListener();
242
  initMap();
243
- loadHotspots();
244
  setupAddHotspotListener();
245
  requestAnimationFrame(update);
246
  }
247
 
248
- tg.onEvent('themeChanged', function() {
249
- document.documentElement.className = tg.colorScheme;
250
- });
251
-
252
- tg.ready();
253
- tg.expand();
254
-
255
- if (tg.initDataUnsafe && tg.initDataUnsafe.user) {
256
- state.telegramUser = tg.initDataUnsafe.user;
257
- initArApp();
258
- } else {
259
- tg.close();
260
- }
261
-
262
  async function setupCamera() {
263
  const video = document.getElementById('camera-view');
264
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
@@ -269,7 +269,7 @@ def index():
269
  video.srcObject = stream;
270
  await video.play();
271
  } catch (err) {
272
- tg.showAlert('Не удалось получить доступ к камере: ' + err.message);
273
  }
274
  }
275
  }
@@ -291,72 +291,63 @@ def index():
291
  }
292
  },
293
  (error) => {
294
- tg.showAlert('Не удалось получить доступ к GPS: ' + error.message);
295
  },
296
  { enableHighAccuracy: true, maximumAge: 1000, timeout: 5000 }
297
  );
298
  } else {
299
- tg.showAlert('GPS не поддерживается вашим устройством.');
300
  }
301
  }
302
 
303
  function setupOrientationListener() {
304
- const isIOS = typeof DeviceOrientationEvent.requestPermission === 'function';
305
-
306
- const startOrientation = () => {
307
- if (window.DeviceOrientationEvent) {
308
- window.addEventListener('deviceorientation', (event) => {
309
- if (event.alpha !== null) {
310
- let alpha = event.webkitCompassHeading || event.alpha;
311
- state.deviceOrientation.smoothedAlpha = state.deviceOrientation.smoothedAlpha * (1 - ORIENTATION_SMOOTHING) + alpha * ORIENTATION_SMOOTHING;
312
- state.deviceOrientation.alpha = alpha;
313
- state.deviceOrientation.beta = event.beta;
314
- state.deviceOrientation.gamma = event.gamma;
315
- }
316
- }, true);
317
- } else {
318
- tg.showAlert('Отслеживание ориентации устройства не поддерживается.');
319
- }
320
- };
321
-
322
- if (isIOS) {
323
- document.body.addEventListener('click', () => {
324
- DeviceOrientationEvent.requestPermission()
325
- .then(permissionState => {
326
- if (permissionState === 'granted') {
327
- startOrientation();
328
- } else {
329
- tg.showAlert('Доступ к ориентации устройства отклонен.');
330
- }
331
- })
332
- .catch(console.error);
333
- }, { once: true });
334
  } else {
335
- startOrientation();
336
  }
337
  }
338
 
339
  function initMap() {
340
- state.map = L.map('map', {zoomControl: false}).setView([0, 0], 2);
341
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
342
  attribution: '© OpenStreetMap'
343
  }).addTo(state.map);
344
-
345
  const userIcon = L.divIcon({
346
- html: '<div></div>',
347
- className: 'user-marker',
348
- iconSize: [20, 20]
 
349
  });
350
-
351
- state.userMarker = L.marker([0, 0]).addTo(state.map).bindPopup('Ваше местоположение');
352
 
353
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
354
  }
355
 
356
  function toggleMap() {
 
357
  const mapContainer = document.getElementById('map-container');
358
- mapContainer.classList.toggle('fullscreen');
359
- tg.HapticFeedback.impactOccurred('light');
 
 
 
 
 
 
 
 
 
 
 
 
360
  setTimeout(() => {
361
  state.map.invalidateSize();
362
  if (state.currentUserPosition) {
@@ -382,23 +373,21 @@ def index():
382
  container.innerHTML = '';
383
  state.hotspots.forEach((hotspot, index) => {
384
  const el = document.createElement('div');
385
- el.className = 'hotspot';
386
  el.id = `hotspot-${index}`;
387
- el.innerHTML = `${hotspot.text}<br><small>@${hotspot.creator_username || 'anonymous'}</small>`;
388
  container.appendChild(el);
389
  });
390
  }
391
 
392
  function renderHotspotsOnMap() {
393
  if (!state.map) return;
394
-
395
  state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
396
  state.hotspotMarkers = [];
397
-
398
  state.hotspots.forEach(hotspot => {
399
  const marker = L.marker([hotspot.lat, hotspot.lon])
400
  .addTo(state.map)
401
- .bindPopup(`${hotspot.text}<br>@${hotspot.creator_username || 'anonymous'}`);
402
  state.hotspotMarkers.push(marker);
403
  });
404
  }
@@ -408,51 +397,76 @@ def index():
408
  container.addEventListener('click', (event) => {
409
  if (event.target !== container) return;
410
 
 
 
411
  if (!state.currentUserPosition) {
412
- tg.showAlert('GPS-координаты еще не определены. Подождите немного.');
413
  return;
414
  }
415
 
416
- tg.showPopup({
417
  title: 'Новый хотспот',
418
- message: 'Введите текст для новой метки в AR.',
419
- buttons: [
420
- {id: 'save', type: 'default', text: 'Сохранить'},
421
- {type: 'cancel'},
422
- ]
423
- }, async (buttonId, text) => {
424
- if (buttonId === 'save' && text) {
425
- const newHotspotData = {
426
- text: text,
427
- lat: state.currentUserPosition.lat,
428
- lon: state.currentUserPosition.lon,
429
- _auth: tg.initData
430
- };
431
-
432
- try {
433
- const response = await fetch('/hotspots', {
434
- method: 'POST',
435
- headers: { 'Content-Type': 'application/json' },
436
- body: JSON.stringify(newHotspotData)
437
- });
438
- if(response.ok) {
439
- const savedHotspot = await response.json();
440
- state.hotspots.push(savedHotspot.hotspot);
441
- renderHotspots();
442
- renderHotspotsOnMap();
443
- tg.HapticFeedback.notificationOccurred('success');
444
- } else {
445
- const errorData = await response.json();
446
- tg.showAlert('Не удалось сохранить хотспот: ' + (errorData.error || 'Server error'));
 
 
 
447
  }
448
- } catch (error) {
449
- console.error('Ошибка сохранения хотспота:', error);
450
- tg.showAlert('Ошибка сети при сохранении хотспота.');
451
  }
452
  }
453
  });
454
- const popupInput = document.querySelector('.telegram-popup-input');
455
- if (popupInput) popupInput.placeholder = 'Ваше сообщение...';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  });
457
  }
458
 
@@ -461,10 +475,9 @@ def index():
461
  const R = 6371e3;
462
  const dLat = toRad(coords2.lat - coords1.lat);
463
  const dLon = toRad(coords2.lon - coords1.lon);
464
- const lat1 = toRad(coords1.lat);
465
- const lat2 = toRad(coords2.lat);
466
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
467
- Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
 
468
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
469
  return R * c;
470
  }
@@ -478,19 +491,32 @@ def index():
478
  const lon2 = toRad(end.lon);
479
  const y = Math.sin(lon2 - lon1) * Math.cos(lat2);
480
  const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
481
- let brng = toDeg(Math.atan2(y, x));
482
- return (brng + 360) % 360;
 
 
 
 
 
 
 
 
 
 
 
 
483
  }
484
 
485
  function update() {
486
- if (!state.currentUserPosition) {
487
  requestAnimationFrame(update);
488
  return;
489
  }
490
 
 
 
491
  const screenWidth = window.innerWidth;
492
  const screenHeight = window.innerHeight;
493
- const halfFovRad = (state.cameraFov / 2) * (Math.PI / 180);
494
 
495
  state.hotspots.forEach((hotspot, index) => {
496
  const el = document.getElementById(`hotspot-${index}`);
@@ -504,23 +530,20 @@ def index():
504
  }
505
 
506
  const bearing = calculateBearing(state.currentUserPosition, hotspot);
507
- let angleDiff = bearing - state.deviceOrientation.smoothedAlpha;
508
 
509
  if (angleDiff > 180) angleDiff -= 360;
510
  if (angleDiff < -180) angleDiff += 360;
511
-
512
  if (Math.abs(angleDiff) > state.cameraFov / 2) {
513
  el.classList.add('hidden');
514
  } else {
515
  el.classList.remove('hidden');
516
 
517
  const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
 
518
 
519
- const pitch = state.deviceOrientation.beta || 90;
520
- const pitchRad = (pitch - 90) * (Math.PI / 180);
521
- const y = screenHeight / 2 - Math.tan(pitchRad) * (screenHeight / 2);
522
-
523
- const scale = Math.max(0.4, 1.5 - (distance / 200));
524
 
525
  el.style.left = `${x}px`;
526
  el.style.top = `${y}px`;
@@ -537,6 +560,42 @@ def index():
537
  '''
538
  return Response(html_content, mimetype='text/html')
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  @app.route('/hotspots', methods=['GET', 'POST'])
541
  def handle_hotspots():
542
  if request.method == 'GET':
@@ -547,29 +606,22 @@ def handle_hotspots():
547
  return jsonify({"error": "Missing JSON in request"}), 400
548
 
549
  data = request.get_json()
550
- init_data_str = data.get('_auth')
551
-
552
- if not init_data_str:
553
- return jsonify({"error": "Authentication data missing"}), 401
554
-
555
- user_data = validate_init_data(init_data_str, BOT_TOKEN)
556
- if not user_data:
557
- return jsonify({"error": "Authentication failed"}), 403
558
-
559
  text = data.get('text')
560
  lat = data.get('lat')
561
  lon = data.get('lon')
 
 
562
 
563
- if not all([text, lat, lon]):
564
- return jsonify({"error": "Missing data: text, lat, or lon"}), 400
565
 
566
  try:
567
  new_hotspot = {
568
  "text": str(text),
569
  "lat": float(lat),
570
  "lon": float(lon),
571
- "creator_id": user_data.get('id'),
572
- "creator_username": user_data.get('username', 'anonymous')
573
  }
574
  save_hotspot(new_hotspot)
575
  return jsonify({"success": True, "hotspot": new_hotspot}), 201
 
1
  from flask import Flask, Response, request, jsonify
2
+ import requests
3
  import json
4
  import os
5
  import hmac
6
  import hashlib
7
+ import urllib.parse
8
 
9
  app = Flask(__name__)
10
 
11
  HOTSPOTS_FILE = 'hotspots.json'
12
+ BOT_TOKEN = '6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  def get_all_hotspots():
15
  if not os.path.exists(HOTSPOTS_FILE):
 
33
  <html lang="ru">
34
  <head>
35
  <meta charset="UTF-8">
36
+ <title>TG AR Hotspots</title>
37
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
38
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
39
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
40
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
41
  <style>
42
  :root {
43
+ --tg-bg-color: var(--tg-theme-bg-color, #000);
44
+ --tg-text-color: var(--tg-theme-text-color, #fff);
 
45
  --tg-button-color: var(--tg-theme-button-color, #007aff);
46
+ --tg-button-text-color: var(--tg-theme-button-text-color, #fff);
47
+ --tg-hint-color: var(--tg-theme-hint-color, #aaa);
48
+ --tg-secondary-bg-color: var(--tg-theme-secondary-bg-color, #1c1c1e);
49
  }
50
  body, html {
51
  margin: 0;
 
55
  overflow: hidden;
56
  background-color: var(--tg-bg-color);
57
  color: var(--tg-text-color);
58
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
59
  }
60
+ #auth-wall {
61
  position: fixed;
62
  top: 0;
63
  left: 0;
64
  width: 100%;
65
  height: 100%;
66
+ background-color: var(--tg-bg-color);
67
+ z-index: 200;
68
  display: flex;
69
  flex-direction: column;
70
  align-items: center;
71
  justify-content: center;
72
+ transition: opacity 0.5s ease;
 
73
  text-align: center;
74
+ }
75
+ #auth-wall.hidden {
76
+ opacity: 0;
77
+ pointer-events: none;
78
+ }
79
+ #main-content {
80
+ display: none;
81
+ width: 100%;
82
+ height: 100%;
83
  }
84
  #ar-container {
85
  position: absolute;
 
100
  }
101
  .hotspot {
102
  position: absolute;
103
+ background-color: rgba(0, 122, 255, 0.9);
104
+ color: white;
105
  padding: 10px 15px;
106
  border-radius: 12px;
107
+ border: 1px solid rgba(255, 255, 255, 0.3);
108
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
109
  transform: translate(-50%, -50%);
110
  transition: opacity 0.3s, transform 0.1s linear;
111
  white-space: nowrap;
112
  font-size: 16px;
113
+ font-weight: 500;
114
  will-change: transform, left, top, opacity;
115
  z-index: 10;
116
  text-align: center;
117
+ backdrop-filter: blur(10px);
118
+ -webkit-backdrop-filter: blur(10px);
119
  }
120
  .hotspot small {
121
+ font-size: 0.75em;
122
  opacity: 0.8;
123
  display: block;
124
  margin-top: 5px;
125
+ font-weight: 400;
126
  }
127
  .hotspot.hidden {
128
  opacity: 0;
 
133
  bottom: 20px;
134
  left: 50%;
135
  transform: translateX(-50%);
136
+ width: 200px;
137
+ height: 200px;
138
+ background-color: var(--tg-secondary-bg-color);
 
 
139
  border-radius: 15px;
140
  overflow: hidden;
141
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
142
  z-index: 50;
143
  display: flex;
144
  flex-direction: column;
145
+ border: 1px solid var(--tg-hint-color);
146
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
147
  }
148
  #map-container.fullscreen {
149
  width: 100%;
 
153
  bottom: 0;
154
  border-radius: 0;
155
  transform: none;
156
+ border: none;
 
 
 
157
  }
158
  #map {
159
  flex-grow: 1;
160
  width: 100%;
161
  height: 100%;
 
 
162
  }
163
+ #map-controls {
164
+ position: absolute;
165
+ top: 10px;
166
+ right: 10px;
167
+ z-index: 1000;
168
  }
169
  #toggle-map-button {
170
+ background-color: var(--tg-button-color);
171
+ color: var(--tg-button-text-color);
 
 
 
172
  border: none;
173
+ padding: 8px 12px;
174
+ border-radius: 8px;
175
  cursor: pointer;
176
  font-size: 14px;
177
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
 
 
 
 
 
 
 
 
178
  }
179
  </style>
180
  </head>
181
  <body>
182
+ <div id="auth-wall">
183
+ <h1>AR Хотспоты</h1>
184
+ <p>Авторизация через Telegram...</p>
185
+ </div>
186
+
187
+ <div id="main-content">
188
  <video id="camera-view" playsinline autoplay muted></video>
189
  <div id="ar-container"></div>
190
  <div id="map-container">
191
  <div id="map"></div>
192
+ <div id="map-controls">
193
+ <button id="toggle-map-button">Карта</button>
194
+ </div>
195
  </div>
196
  </div>
197
 
 
 
 
 
 
198
  <script>
 
 
199
  const state = {
200
  hotspots: [],
201
+ currentUser: null,
202
  currentUserPosition: null,
203
+ deviceOrientation: { alpha: 0, beta: 0, gamma: 0 },
204
+ smoothedOrientation: { alpha: 0, beta: 0, gamma: 0 },
205
  cameraFov: 60,
206
  map: null,
207
  userMarker: null,
208
  hotspotMarkers: [],
209
  initialMapSet: false,
210
+ isMapFullscreen: false,
211
  };
212
 
213
+ const SMOOTHING_FACTOR = 0.05;
214
+ const MAX_VISIBLE_DISTANCE = 2000;
215
 
216
+ window.addEventListener('load', function() {
217
+ const tg = window.Telegram.WebApp;
218
+ tg.ready();
219
+ tg.expand();
220
+ authorizeUser(tg.initData);
221
+ });
222
+
223
+ async function authorizeUser(initData) {
224
+ if (!initData) {
225
+ document.getElementById('auth-wall').innerHTML = '<h1>Ошибка</h1><p>Не удалось получить данные Telegram. Пожалуйста, откройте приложение через Telegram.</p>';
226
+ return;
227
+ }
228
+
229
+ try {
230
+ const response = await fetch('/validate_init_data', {
231
+ method: 'POST',
232
+ headers: { 'Content-Type': 'application/json' },
233
+ body: JSON.stringify({ initData: initData })
234
+ });
235
+
236
+ if (response.ok) {
237
+ const userData = await response.json();
238
+ state.currentUser = userData;
239
+ initArApp();
240
+ } else {
241
+ throw new Error('Validation failed');
242
+ }
243
+ } catch (error) {
244
+ document.getElementById('auth-wall').innerHTML = '<h1>Ошибка</h1><p>Не удалось авторизоваться. Попробуйте перезапустить приложение.</p>';
245
+ console.error('Authorization error:', error);
246
+ }
247
+ }
248
+
249
+ async function initArApp() {
250
+ document.getElementById('auth-wall').classList.add('hidden');
251
+ document.getElementById('main-content').style.display = 'block';
252
+
253
+ await setupCamera();
254
  setupGPS();
255
  setupOrientationListener();
256
  initMap();
257
+ await loadHotspots();
258
  setupAddHotspotListener();
259
  requestAnimationFrame(update);
260
  }
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  async function setupCamera() {
263
  const video = document.getElementById('camera-view');
264
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
 
269
  video.srcObject = stream;
270
  await video.play();
271
  } catch (err) {
272
+ alert('Не удалось получить доступ к камере: ' + err);
273
  }
274
  }
275
  }
 
291
  }
292
  },
293
  (error) => {
294
+ alert('Не удалось получить доступ к GPS: ' + error.message);
295
  },
296
  { enableHighAccuracy: true, maximumAge: 1000, timeout: 5000 }
297
  );
298
  } else {
299
+ alert('GPS не поддерживается вашим браузером.');
300
  }
301
  }
302
 
303
  function setupOrientationListener() {
304
+ if (window.DeviceOrientationEvent) {
305
+ window.addEventListener('deviceorientationabsolute', (event) => {
306
+ if (event.alpha !== null) {
307
+ state.deviceOrientation.alpha = event.alpha;
308
+ state.deviceOrientation.beta = event.beta;
309
+ state.deviceOrientation.gamma = event.gamma;
310
+ }
311
+ }, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  } else {
313
+ alert('Отслеживание ориентации устройства не поддерживается.');
314
  }
315
  }
316
 
317
  function initMap() {
318
+ state.map = L.map('map', { zoomControl: false }).setView([0, 0], 2);
319
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
320
  attribution: '© OpenStreetMap'
321
  }).addTo(state.map);
322
+
323
  const userIcon = L.divIcon({
324
+ html: '<div style="background-color: #007aff; width: 16px; height: 16px; border-radius: 50%; border: 3px solid white; box-shadow: 0 0 10px #007aff;"></div>',
325
+ className: '',
326
+ iconSize: [22, 22],
327
+ iconAnchor: [11, 11]
328
  });
329
+ state.userMarker = L.marker([0, 0], {icon: userIcon}).addTo(state.map).bindPopup('Это вы');
 
330
 
331
  document.getElementById('toggle-map-button').addEventListener('click', toggleMap);
332
  }
333
 
334
  function toggleMap() {
335
+ window.Telegram.WebApp.HapticFeedback.impactOccurred('light');
336
  const mapContainer = document.getElementById('map-container');
337
+ const btn = document.getElementById('toggle-map-button');
338
+ state.isMapFullscreen = !state.isMapFullscreen;
339
+
340
+ mapContainer.classList.toggle('fullscreen', state.isMapFullscreen);
341
+ btn.innerText = state.isMapFullscreen ? 'Свернуть' : 'Карта';
342
+
343
+ if (state.isMapFullscreen) {
344
+ document.getElementById('ar-container').style.display = 'none';
345
+ document.getElementById('camera-view').style.display = 'none';
346
+ } else {
347
+ document.getElementById('ar-container').style.display = 'block';
348
+ document.getElementById('camera-view').style.display = 'block';
349
+ }
350
+
351
  setTimeout(() => {
352
  state.map.invalidateSize();
353
  if (state.currentUserPosition) {
 
373
  container.innerHTML = '';
374
  state.hotspots.forEach((hotspot, index) => {
375
  const el = document.createElement('div');
376
+ el.className = 'hotspot hidden';
377
  el.id = `hotspot-${index}`;
378
+ el.innerHTML = `${hotspot.text}<br><small>@${hotspot.creator_username || 'unknown'}</small>`;
379
  container.appendChild(el);
380
  });
381
  }
382
 
383
  function renderHotspotsOnMap() {
384
  if (!state.map) return;
 
385
  state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
386
  state.hotspotMarkers = [];
 
387
  state.hotspots.forEach(hotspot => {
388
  const marker = L.marker([hotspot.lat, hotspot.lon])
389
  .addTo(state.map)
390
+ .bindPopup(`<b>${hotspot.text}</b><br>от @${hotspot.creator_username || 'unknown'}`);
391
  state.hotspotMarkers.push(marker);
392
  });
393
  }
 
397
  container.addEventListener('click', (event) => {
398
  if (event.target !== container) return;
399
 
400
+ window.Telegram.WebApp.HapticFeedback.impactOccurred('medium');
401
+
402
  if (!state.currentUserPosition) {
403
+ window.Telegram.WebApp.showAlert('GPS-координаты еще не определены. Подождите немного.');
404
  return;
405
  }
406
 
407
+ window.Telegram.WebApp.showPopup({
408
  title: 'Новый хотспот',
409
+ message: 'Введите текст для новой AR-метки:',
410
+ buttons: [{id: 'send', type: 'default', text: 'Создать'}, {type: 'cancel'}]
411
+ }, async (buttonId) => {
412
+ if (buttonId === 'send') {
413
+ const text = window.Telegram.WebApp.Popup.text;
414
+ if (text) {
415
+ const newHotspot = {
416
+ text: text,
417
+ lat: state.currentUserPosition.lat,
418
+ lon: state.currentUserPosition.lon,
419
+ creator_id: state.currentUser.id,
420
+ creator_username: state.currentUser.username
421
+ };
422
+
423
+ try {
424
+ const response = await fetch('/hotspots', {
425
+ method: 'POST',
426
+ headers: { 'Content-Type': 'application/json' },
427
+ body: JSON.stringify(newHotspot)
428
+ });
429
+ if(response.ok) {
430
+ const savedHotspot = await response.json();
431
+ state.hotspots.push(savedHotspot.hotspot);
432
+ renderHotspots();
433
+ renderHotspotsOnMap();
434
+ window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
435
+ } else {
436
+ window.Telegram.WebApp.showAlert('Не удалось сохранить хотспот.');
437
+ }
438
+ } catch (error) {
439
+ console.error('Ош��бка сохранения хотспота:', error);
440
+ window.Telegram.WebApp.showAlert('Ошибка сети при сохранении хотспота.');
441
  }
 
 
 
442
  }
443
  }
444
  });
445
+ // This is a workaround to make the popup appear with a text input field
446
+ setTimeout(() => {
447
+ const popup = document.querySelector('.popup-body');
448
+ if (popup) {
449
+ const input = document.createElement('input');
450
+ input.type = 'text';
451
+ input.className = 'popup-input';
452
+ input.placeholder = 'Ваш текст здесь...';
453
+ input.style.width = '90%';
454
+ input.style.padding = '10px';
455
+ input.style.margin = '10px auto';
456
+ input.style.border = '1px solid var(--tg-theme-hint-color)';
457
+ input.style.borderRadius = '5px';
458
+ input.style.backgroundColor = 'var(--tg-theme-bg-color)';
459
+ input.style.color = 'var(--tg-theme-text-color)';
460
+
461
+ popup.insertBefore(input, popup.firstChild);
462
+ input.focus();
463
+
464
+ Object.defineProperty(window.Telegram.WebApp.Popup, 'text', {
465
+ get: () => input.value,
466
+ configurable: true
467
+ });
468
+ }
469
+ }, 50);
470
  });
471
  }
472
 
 
475
  const R = 6371e3;
476
  const dLat = toRad(coords2.lat - coords1.lat);
477
  const dLon = toRad(coords2.lon - coords1.lon);
 
 
478
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
479
+ Math.cos(toRad(coords1.lat)) * Math.cos(toRad(coords2.lat)) *
480
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
481
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
482
  return R * c;
483
  }
 
491
  const lon2 = toRad(end.lon);
492
  const y = Math.sin(lon2 - lon1) * Math.cos(lat2);
493
  const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
494
+ return (toDeg(Math.atan2(y, x)) + 360) % 360;
495
+ }
496
+
497
+ function circularLerp(start, end, amount) {
498
+ let difference = Math.abs(end - start);
499
+ if (difference > 180) {
500
+ if (end > start) {
501
+ start += 360;
502
+ } else {
503
+ end += 360;
504
+ }
505
+ }
506
+ let value = (start + ((end - start) * amount));
507
+ return value % 360;
508
  }
509
 
510
  function update() {
511
+ if (!state.currentUserPosition || state.isMapFullscreen) {
512
  requestAnimationFrame(update);
513
  return;
514
  }
515
 
516
+ state.smoothedOrientation.alpha = circularLerp(state.smoothedOrientation.alpha, state.deviceOrientation.alpha, SMOOTHING_FACTOR);
517
+
518
  const screenWidth = window.innerWidth;
519
  const screenHeight = window.innerHeight;
 
520
 
521
  state.hotspots.forEach((hotspot, index) => {
522
  const el = document.getElementById(`hotspot-${index}`);
 
530
  }
531
 
532
  const bearing = calculateBearing(state.currentUserPosition, hotspot);
533
+ let angleDiff = bearing - state.smoothedOrientation.alpha;
534
 
535
  if (angleDiff > 180) angleDiff -= 360;
536
  if (angleDiff < -180) angleDiff += 360;
537
+
538
  if (Math.abs(angleDiff) > state.cameraFov / 2) {
539
  el.classList.add('hidden');
540
  } else {
541
  el.classList.remove('hidden');
542
 
543
  const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
544
+ const y = screenHeight / 2 - 50;
545
 
546
+ const scale = Math.max(0.5, 1.2 - (distance / MAX_VISIBLE_DISTANCE));
 
 
 
 
547
 
548
  el.style.left = `${x}px`;
549
  el.style.top = `${y}px`;
 
560
  '''
561
  return Response(html_content, mimetype='text/html')
562
 
563
+ @app.route('/validate_init_data', methods=['POST'])
564
+ def validate_init_data():
565
+ data = request.get_json()
566
+ init_data = data.get('initData')
567
+
568
+ if not init_data:
569
+ return jsonify({'error': 'No initData provided'}), 400
570
+
571
+ try:
572
+ parsed_data = urllib.parse.parse_qsl(init_data)
573
+ data_dict = dict(parsed_data)
574
+
575
+ received_hash = data_dict.pop('hash', None)
576
+ if not received_hash:
577
+ return jsonify({'error': 'Hash not found in initData'}), 400
578
+
579
+ data_check_string = "\n".join(sorted([f"{k}={v}" for k, v in data_dict.items()]))
580
+
581
+ secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
582
+ calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
583
+
584
+ if calculated_hash == received_hash:
585
+ user_data = json.loads(data_dict.get('user', '{}'))
586
+ return jsonify({
587
+ 'id': user_data.get('id'),
588
+ 'first_name': user_data.get('first_name'),
589
+ 'last_name': user_data.get('last_name'),
590
+ 'username': user_data.get('username'),
591
+ 'language_code': user_data.get('language_code'),
592
+ }), 200
593
+ else:
594
+ return jsonify({'error': 'Invalid hash'}), 403
595
+
596
+ except Exception as e:
597
+ return jsonify({'error': str(e)}), 500
598
+
599
  @app.route('/hotspots', methods=['GET', 'POST'])
600
  def handle_hotspots():
601
  if request.method == 'GET':
 
606
  return jsonify({"error": "Missing JSON in request"}), 400
607
 
608
  data = request.get_json()
 
 
 
 
 
 
 
 
 
609
  text = data.get('text')
610
  lat = data.get('lat')
611
  lon = data.get('lon')
612
+ creator_id = data.get('creator_id')
613
+ creator_username = data.get('creator_username')
614
 
615
+ if not all([text, lat, lon, creator_id]):
616
+ return jsonify({"error": "Missing data"}), 400
617
 
618
  try:
619
  new_hotspot = {
620
  "text": str(text),
621
  "lat": float(lat),
622
  "lon": float(lon),
623
+ "creator_id": int(creator_id),
624
+ "creator_username": str(creator_username) if creator_username else None
625
  }
626
  save_hotspot(new_hotspot)
627
  return jsonify({"success": True, "hotspot": new_hotspot}), 201