test2 / app.py
Aleksmorshen's picture
Update app.py
a408963 verified
from flask import Flask, Response, request, jsonify
import json
import os
import hmac
import hashlib
from urllib.parse import unquote
app = Flask(__name__)
HOTSPOTS_FILE = 'hotspots.json'
BOT_TOKEN = '6909363967:AAGl58czIt7Vra_V8wWR7MyXkN_ayS27Soo'
def get_all_hotspots():
if not os.path.exists(HOTSPOTS_FILE):
return []
try:
with open(HOTSPOTS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
return []
def save_hotspot(new_hotspot):
hotspots = get_all_hotspots()
hotspots.append(new_hotspot)
with open(HOTSPOTS_FILE, 'w', encoding='utf-8') as f:
json.dump(hotspots, f, ensure_ascii=False, indent=4)
def is_data_safe(init_data: str) -> (bool, dict):
try:
encoded_data = unquote(init_data)
data_check_string = []
recieved_hash = ''
for pair in encoded_data.split('&'):
key, value = pair.split('=', 1)
if key == 'hash':
recieved_hash = value
else:
data_check_string.append(f"{key}={value}")
data_check_string.sort()
data_check_string = "\n".join(data_check_string)
secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
if calculated_hash == recieved_hash:
data = {k: v for k, v in [pair.split('=', 1) for pair in encoded_data.split('&')]}
user_data = json.loads(unquote(data['user']))
return True, user_data
return False, None
except Exception:
return False, None
@app.route('/')
def index():
html_content = '''
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>TON AR Hotspots</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
:root {
--tg-bg-color: var(--tg-theme-bg-color, #000);
--tg-text-color: var(--tg-theme-text-color, #fff);
--tg-hint-color: var(--tg-theme-hint-color, #aaa);
--tg-button-color: var(--tg-theme-button-color, #007aff);
--tg-button-text-color: var(--tg-theme-button-text-color, #fff);
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: var(--tg-bg-color);
color: var(--tg-text-color);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
#ar-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#camera-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.hotspot {
position: absolute;
background-color: rgba(0, 122, 255, 0.85);
color: white;
padding: 10px 15px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
transform: translate(-50%, -50%);
transition: opacity 0.3s, transform 0.1s linear;
white-space: nowrap;
font-size: 16px;
font-weight: 500;
will-change: transform, left, top, opacity;
z-index: 10;
text-align: center;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.hotspot small {
font-size: 0.75em;
opacity: 0.85;
display: block;
margin-top: 5px;
font-weight: 400;
}
.hotspot.hidden {
opacity: 0;
pointer-events: none;
}
#map-container {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 220px;
height: 150px;
border-radius: 15px;
overflow: hidden;
transition: all 0.3s ease-in-out;
z-index: 50;
border: 1px solid var(--tg-hint-color);
box-shadow: 0 5px 25px rgba(0,0,0,0.5);
}
#map-container.fullscreen {
width: 100vw;
height: 100vh;
top: 0;
left: 0;
bottom: 0;
border-radius: 0;
transform: none;
border: none;
}
#map {
width: 100%;
height: 100%;
}
.leaflet-control-container .leaflet-control-attribution {
display: none;
}
#welcome-message {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0,0,0,0.6);
padding: 8px 15px;
border-radius: 20px;
z-index: 51;
font-size: 14px;
backdrop-filter: blur(5px);
}
</style>
</head>
<body>
<div id="welcome-message"></div>
<video id="camera-view" playsinline autoplay muted></video>
<div id="ar-container"></div>
<div id="map-container">
<div id="map"></div>
</div>
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
const state = {
hotspots: [],
currentUserPosition: null,
deviceOrientation: { alpha: 0, beta: 0, gamma: 0 },
smoothedAlpha: 0,
smoothingFactor: 0.05,
cameraFov: 60,
map: null,
userMarker: null,
hotspotMarkers: [],
initialMapSet: false,
userInfo: tg.initDataUnsafe.user || {}
};
const MAX_VISIBLE_DISTANCE = 1000;
function formatUserName(user) {
if (!user) return "Аноним";
let name = user.first_name || '';
if (user.last_name) name += ` ${user.last_name}`;
if (user.username) name += ` (@${user.username})`;
return name.trim();
}
document.getElementById('welcome-message').innerText = `Привет, ${state.userInfo.first_name || 'пользователь'}!`;
async function initArApp() {
await setupCamera();
setupGPS();
setupOrientationListener();
initMap();
await loadHotspots();
setupAddHotspotListener();
requestAnimationFrame(update);
}
async function setupCamera() {
const video = document.getElementById('camera-view');
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
video.srcObject = stream;
await video.play();
} catch (err) {
tg.showAlert('Не удалось получить доступ к камере: ' + err.message);
}
}
}
function setupGPS() {
if (navigator.geolocation) {
navigator.geolocation.watchPosition(
(position) => {
state.currentUserPosition = {
lat: position.coords.latitude,
lon: position.coords.longitude
};
if (state.userMarker) {
state.userMarker.setLatLng([state.currentUserPosition.lat, state.currentUserPosition.lon]);
if (!state.initialMapSet) {
state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], 16);
state.initialMapSet = true;
}
}
},
(error) => {
tg.showAlert('Не удалось получить доступ к GPS: ' + error.message);
},
{ enableHighAccuracy: true, maximumAge: 0, timeout: 5000 }
);
} else {
tg.showAlert('GPS не поддерживается вашим устройством.');
}
}
function setupOrientationListener() {
const handleOrientation = (event) => {
if (event.alpha !== null) {
state.deviceOrientation.alpha = event.alpha;
state.deviceOrientation.beta = event.beta;
state.deviceOrientation.gamma = event.gamma;
let alpha = event.webkitCompassHeading || event.alpha;
let diff = alpha - state.smoothedAlpha;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
state.smoothedAlpha += state.smoothingFactor * diff;
state.smoothedAlpha = (state.smoothedAlpha + 360) % 360;
}
};
if (window.DeviceOrientationEvent) {
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission()
.then(permissionState => {
if (permissionState === 'granted') {
window.addEventListener('deviceorientation', handleOrientation, true);
} else {
tg.showAlert('Доступ к ориентации устройства отклонен.');
}
})
.catch(console.error);
} else {
window.addEventListener('deviceorientation', handleOrientation, true);
}
} else {
tg.showAlert('Отслеживание ориентации устройства не поддерживается.');
}
}
function initMap() {
state.map = L.map('map', { zoomControl: false }).setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(state.map);
var userIcon = L.divIcon({
className: 'user-location-dot',
html: '<div style="background-color: #007aff; width: 12px; height: 12px; border-radius: 50%; border: 2px solid white;"></div>',
iconSize: [16, 16],
iconAnchor: [8, 8]
});
state.userMarker = L.marker([0, 0], { icon: userIcon }).addTo(state.map);
document.getElementById('map-container').addEventListener('click', () => {
const mapContainer = document.getElementById('map-container');
mapContainer.classList.toggle('fullscreen');
setTimeout(() => {
state.map.invalidateSize();
if (state.currentUserPosition) {
state.map.setView([state.currentUserPosition.lat, state.currentUserPosition.lon], state.map.getZoom());
}
}, 310);
});
}
async function loadHotspots() {
try {
const response = await fetch('/hotspots');
const data = await response.json();
state.hotspots = data;
renderHotspots();
renderHotspotsOnMap();
} catch (error) {
tg.showAlert('Ошибка загрузки хотспотов: ' + error.message);
}
}
function renderHotspots() {
const container = document.getElementById('ar-container');
container.innerHTML = '';
state.hotspots.forEach((hotspot, index) => {
const el = document.createElement('div');
el.className = 'hotspot';
el.id = `hotspot-${index}`;
el.innerHTML = `${hotspot.text}<br><small>by ${hotspot.creator_info || 'Unknown'}</small>`;
container.appendChild(el);
});
}
function renderHotspotsOnMap() {
if (!state.map) return;
state.hotspotMarkers.forEach(marker => state.map.removeLayer(marker));
state.hotspotMarkers = [];
state.hotspots.forEach(hotspot => {
const marker = L.marker([hotspot.lat, hotspot.lon])
.addTo(state.map)
.bindPopup(`<b>${hotspot.text}</b><br>by ${hotspot.creator_info || 'Unknown'}`);
state.hotspotMarkers.push(marker);
});
}
function setupAddHotspotListener() {
document.body.addEventListener('dblclick', (event) => {
if (event.target.closest('#map-container')) return;
tg.showPopup({
title: 'Новый хотспот',
message: 'Введите текст для новой AR-метки. Она будет создана в вашем текущем местоположении.',
buttons: [
{id: 'create', type: 'default', text: 'Создать'},
{type: 'cancel'},
]
}, async (buttonId) => {
if (buttonId === 'create') {
const text = prompt("Введите текст для хотспота:");
if (text && text.trim() !== '') {
await createHotspot(text.trim());
}
}
});
});
}
async function createHotspot(text) {
if (!state.currentUserPosition) {
tg.showAlert('GPS-координаты еще не определены. Подождите и попробуйте снова.');
return;
}
const newHotspotData = {
text: text,
lat: state.currentUserPosition.lat,
lon: state.currentUserPosition.lon,
initData: tg.initData
};
try {
const response = await fetch('/hotspots', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newHotspotData)
});
if (response.ok) {
const savedHotspot = await response.json();
state.hotspots.push(savedHotspot.hotspot);
renderHotspots();
renderHotspotsOnMap();
tg.HapticFeedback.notificationOccurred('success');
} else {
const error = await response.json();
tg.showAlert(`Не удалось сохранить хотспот: ${error.error}`);
}
} catch (error) {
tg.showAlert('Сетевая ошибка при сохранении хотспота.');
}
}
function haversineDistance(coords1, coords2) {
function toRad(x) { return x * Math.PI / 180; }
const R = 6371000;
const dLat = toRad(coords2.lat - coords1.lat);
const dLon = toRad(coords2.lon - coords1.lon);
const lat1 = toRad(coords1.lat);
const lat2 = toRad(coords2.lat);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function calculateBearing(start, end) {
function toRad(x) { return x * Math.PI / 180; }
function toDeg(x) { return x * 180 / Math.PI; }
const y = Math.sin(toRad(end.lon - start.lon)) * Math.cos(toRad(end.lat));
const x = Math.cos(toRad(start.lat)) * Math.sin(toRad(end.lat)) -
Math.sin(toRad(start.lat)) * Math.cos(toRad(end.lat)) * Math.cos(toRad(end.lon - start.lon));
let brng = toDeg(Math.atan2(y, x));
return (brng + 360) % 360;
}
function update() {
if (!state.currentUserPosition) {
requestAnimationFrame(update);
return;
}
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
state.hotspots.forEach((hotspot, index) => {
const el = document.getElementById(`hotspot-${index}`);
if (!el) return;
const distance = haversineDistance(state.currentUserPosition, hotspot);
if (distance > MAX_VISIBLE_DISTANCE) {
el.classList.add('hidden');
return;
}
const bearing = calculateBearing(state.currentUserPosition, hotspot);
let angleDiff = bearing - state.smoothedAlpha;
if (angleDiff > 180) angleDiff -= 360;
if (angleDiff < -180) angleDiff += 360;
if (Math.abs(angleDiff) > state.cameraFov / 2) {
el.classList.add('hidden');
} else {
el.classList.remove('hidden');
const x = screenWidth / 2 + (angleDiff / (state.cameraFov / 2)) * (screenWidth / 2);
const verticalAngle = -state.deviceOrientation.beta;
const y = screenHeight / 2 - (Math.tan(verticalAngle * Math.PI / 180) * (screenHeight / 2));
const scale = Math.max(0.4, 1.2 - distance / MAX_VISIBLE_DISTANCE);
el.style.left = `${x}px`;
el.style.top = `${y}px`;
el.style.transform = `translate(-50%, -50%) scale(${scale})`;
el.style.zIndex = Math.round(10000 - distance);
}
});
requestAnimationFrame(update);
}
initArApp();
</script>
</body>
</html>
'''
return Response(html_content, mimetype='text/html')
@app.route('/hotspots', methods=['GET', 'POST'])
def handle_hotspots():
if request.method == 'GET':
return jsonify(get_all_hotspots())
if request.method == 'POST':
if not request.is_json:
return jsonify({"error": "Missing JSON in request"}), 400
data = request.get_json()
init_data = data.get('initData')
if not init_data:
return jsonify({"error": "Missing initData"}), 401
is_safe, user_data = is_data_safe(init_data)
if not is_safe:
return jsonify({"error": "Validation failed"}), 403
text = data.get('text')
lat = data.get('lat')
lon = data.get('lon')
if not all([text, lat, lon]):
return jsonify({"error": "Missing data: text, lat, or lon"}), 400
try:
creator_info = user_data.get('first_name', 'User')
if user_data.get('username'):
creator_info = f"{creator_info} (@{user_data.get('username')})"
new_hotspot = {
"text": str(text),
"lat": float(lat),
"lon": float(lon),
"creator_info": creator_info,
"creator_id": user_data.get('id')
}
save_hotspot(new_hotspot)
return jsonify({"success": True, "hotspot": new_hotspot}), 201
except (ValueError, TypeError):
return jsonify({"error": "Invalid data types"}), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=False)