Update app.py
Browse files
app.py
CHANGED
|
@@ -1,19 +1,60 @@
|
|
| 1 |
from flask import Flask, request, jsonify, Response
|
| 2 |
import json
|
| 3 |
-
from pywebpush import webpush
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
app = Flask(__name__)
|
| 6 |
PORT = 7860
|
| 7 |
|
| 8 |
# Путь к файлу для хранения новостей
|
| 9 |
NEWS_FILE = 'news.json'
|
| 10 |
-
# Путь к файлу для хранения подписок
|
| 11 |
SUBSCRIPTIONS_FILE = 'subscriptions.json'
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# --- Вспомогательные функции ---
|
| 19 |
|
|
@@ -46,7 +87,7 @@ def save_subscriptions(subscriptions):
|
|
| 46 |
def send_push_notification(subscription, message_body):
|
| 47 |
"""Отправляет push уведомление одному подписчику."""
|
| 48 |
try:
|
| 49 |
-
print(f"
|
| 50 |
webpush(
|
| 51 |
subscription_info=subscription,
|
| 52 |
data=message_body,
|
|
@@ -54,10 +95,10 @@ def send_push_notification(subscription, message_body):
|
|
| 54 |
vapid_public_key=VAPID_PUBLIC_KEY_BASE64,
|
| 55 |
vapid_claims=VAPID_CLAIMS
|
| 56 |
)
|
| 57 |
-
print(f"Уведомление успешно отправлено на
|
| 58 |
return True
|
| 59 |
-
except
|
| 60 |
-
print(f"Ошибка отправки уведомления на
|
| 61 |
return False
|
| 62 |
|
| 63 |
def send_push_to_all(message_body):
|
|
@@ -68,9 +109,10 @@ def send_push_to_all(message_body):
|
|
| 68 |
if send_push_notification(sub, message_body):
|
| 69 |
valid_subscriptions.append(sub)
|
| 70 |
else:
|
| 71 |
-
print(f"
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 74 |
|
| 75 |
# --- HTML для главной страницы ---
|
| 76 |
HTML_INDEX = """
|
|
@@ -124,16 +166,14 @@ HTML_INDEX = """
|
|
| 124 |
Notification.requestPermission().then(permission => {
|
| 125 |
if (permission === "granted") {
|
| 126 |
console.log("Разрешение на уведомления получено.");
|
| 127 |
-
subscribeUserToPush();
|
| 128 |
} else {
|
| 129 |
console.log("Разрешение на уведомления отклонено.");
|
| 130 |
}
|
| 131 |
});
|
| 132 |
} else if (Notification.permission === "granted") {
|
| 133 |
console.log("Разрешение на уведомления уже есть.");
|
| 134 |
-
subscribeUserToPush();
|
| 135 |
-
} else {
|
| 136 |
-
console.log("Уведомления заблокированы.");
|
| 137 |
}
|
| 138 |
}
|
| 139 |
|
|
@@ -153,10 +193,10 @@ HTML_INDEX = """
|
|
| 153 |
navigator.serviceWorker.ready.then(serviceWorkerRegistration => {
|
| 154 |
serviceWorkerRegistration.pushManager.subscribe({
|
| 155 |
userVisibleOnly: true,
|
| 156 |
-
applicationServerKey: urlBase64ToUint8Array(publicKey)
|
| 157 |
})
|
| 158 |
.then(subscription => {
|
| 159 |
-
console.log('
|
| 160 |
sendSubscriptionToServer(subscription);
|
| 161 |
})
|
| 162 |
.catch(error => {
|
|
@@ -180,10 +220,10 @@ HTML_INDEX = """
|
|
| 180 |
return response.json();
|
| 181 |
})
|
| 182 |
.then(data => {
|
| 183 |
-
console.log('Подписка успешно сохранена
|
| 184 |
})
|
| 185 |
.catch(error => {
|
| 186 |
-
console.error('Ошибка при сохранении подписки
|
| 187 |
});
|
| 188 |
}
|
| 189 |
|
|
@@ -192,13 +232,16 @@ HTML_INDEX = """
|
|
| 192 |
.then(response => response.json())
|
| 193 |
.then(news => {
|
| 194 |
const newsList = document.getElementById('news-list');
|
| 195 |
-
newsList.innerHTML = '';
|
| 196 |
news.forEach(item => {
|
| 197 |
const li = document.createElement('li');
|
| 198 |
li.className = 'list-group-item';
|
| 199 |
li.textContent = item.text;
|
| 200 |
newsList.appendChild(li);
|
| 201 |
});
|
|
|
|
|
|
|
|
|
|
| 202 |
});
|
| 203 |
}
|
| 204 |
|
|
@@ -216,39 +259,34 @@ HTML_INDEX = """
|
|
| 216 |
.then(response => response.json())
|
| 217 |
.then(data => {
|
| 218 |
console.log(data.message);
|
| 219 |
-
document.getElementById('news-text').value = '';
|
| 220 |
-
loadNews();
|
|
|
|
|
|
|
|
|
|
| 221 |
});
|
| 222 |
}
|
| 223 |
}
|
| 224 |
|
| 225 |
-
|
| 226 |
function urlBase64ToUint8Array(base64String) {
|
| 227 |
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
| 228 |
const base64 = (base64String + padding)
|
| 229 |
.replace(/-/g, '+')
|
| 230 |
.replace(/_/g, '/');
|
| 231 |
-
|
| 232 |
const rawData = window.atob(base64);
|
| 233 |
const outputArray = new Uint8Array(rawData.length);
|
| 234 |
-
|
| 235 |
for (let i = 0; i < rawData.length; ++i) {
|
| 236 |
outputArray[i] = rawData.charCodeAt(i);
|
| 237 |
}
|
| 238 |
return outputArray;
|
| 239 |
}
|
| 240 |
|
| 241 |
-
|
| 242 |
-
// Обработка полученных push уведомлений в foreground (если нужно что-то делать на странице)
|
| 243 |
navigator.serviceWorker.addEventListener('message', event => {
|
| 244 |
if (event.data && event.data.type === 'push-received') {
|
| 245 |
-
console.log('
|
| 246 |
-
loadNews();
|
| 247 |
-
// Можно также показать какое-то визуальное уведомление на странице, если нужно
|
| 248 |
}
|
| 249 |
});
|
| 250 |
-
|
| 251 |
-
|
| 252 |
</script>
|
| 253 |
</body>
|
| 254 |
</html>
|
|
@@ -261,12 +299,11 @@ self.addEventListener('push', event => {
|
|
| 261 |
const title = 'Новая новость!';
|
| 262 |
const options = {
|
| 263 |
body: message,
|
| 264 |
-
icon: '/no-icon.png' //
|
| 265 |
};
|
| 266 |
|
| 267 |
event.waitUntil(self.registration.showNotification(title, options));
|
| 268 |
|
| 269 |
-
// Отправляем сообщение клиенту (странице) о получении push уведомления
|
| 270 |
self.clients.matchAll({ type: 'window' }).then(clients => {
|
| 271 |
clients.forEach(client => {
|
| 272 |
client.postMessage({ type: 'push-received', message: message });
|
|
@@ -276,7 +313,7 @@ self.addEventListener('push', event => {
|
|
| 276 |
|
| 277 |
self.addEventListener('notificationclick', event => {
|
| 278 |
event.notification.close();
|
| 279 |
-
event.waitUntil(clients.openWindow('/'));
|
| 280 |
});
|
| 281 |
"""
|
| 282 |
|
|
@@ -292,51 +329,50 @@ MANIFEST_JSON = """
|
|
| 292 |
}
|
| 293 |
"""
|
| 294 |
|
| 295 |
-
|
| 296 |
# --- Flask маршруты ---
|
| 297 |
|
| 298 |
@app.route('/')
|
| 299 |
def index():
|
| 300 |
-
"""Главная страница
|
| 301 |
news = load_news()
|
| 302 |
-
public_key = VAPID_PUBLIC_KEY_BASE64
|
| 303 |
html = HTML_INDEX.replace('%PUBLIC_KEY%', public_key)
|
| 304 |
-
# Заполняем список новостей динамически на сервере, чтобы избежать flicker при загрузке
|
| 305 |
news_list_html = ''.join([f'<li class="list-group-item">{item["text"]}</li>' for item in news])
|
| 306 |
html = html.replace('<ul class="list-group" id="news-list"></ul>', f'<ul class="list-group" id="news-list">{news_list_html}</ul>')
|
| 307 |
return html
|
| 308 |
|
| 309 |
@app.route('/add_news', methods=['POST'])
|
| 310 |
def add_news():
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
send_push_to_all(news_text)
|
| 321 |
-
|
| 322 |
return jsonify({"message": "Новость добавлена и уведомления отправлены"}), 200
|
| 323 |
-
|
|
|
|
|
|
|
| 324 |
|
| 325 |
@app.route('/get_news')
|
| 326 |
def get_news():
|
| 327 |
-
"""API endpoint для получения
|
| 328 |
news = load_news()
|
| 329 |
return jsonify(news)
|
| 330 |
|
| 331 |
@app.route('/save_subscription', methods=['POST'])
|
| 332 |
def save_subscription():
|
| 333 |
-
"""API endpoint для сохранения подписки
|
| 334 |
subscription_data = request.get_json()
|
| 335 |
if subscription_data:
|
| 336 |
subscriptions = load_subscriptions()
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
|
|
|
| 340 |
return jsonify({"message": "Подписка сохранена"}), 201
|
| 341 |
return jsonify({"error": "Неверные данные подписки"}), 400
|
| 342 |
|
|
@@ -350,6 +386,5 @@ def manifest():
|
|
| 350 |
"""Маршрут для manifest.json."""
|
| 351 |
return Response(MANIFEST_JSON, mimetype='application/json')
|
| 352 |
|
| 353 |
-
|
| 354 |
if __name__ == '__main__':
|
| 355 |
-
app.run(debug=True, port=PORT, host='0.0.0.0')
|
|
|
|
| 1 |
from flask import Flask, request, jsonify, Response
|
| 2 |
import json
|
| 3 |
+
from pywebpush import webpush, WebPushException
|
| 4 |
+
import os
|
| 5 |
+
from cryptography.hazmat.primitives import serialization
|
| 6 |
+
from cryptography.hazmat.primitives.asymmetric import ec
|
| 7 |
+
from base64 import urlsafe_b64encode
|
| 8 |
|
| 9 |
app = Flask(__name__)
|
| 10 |
PORT = 7860
|
| 11 |
|
| 12 |
# Путь к файлу для хранения новостей
|
| 13 |
NEWS_FILE = 'news.json'
|
| 14 |
+
# Путь к файлу для хранения подписок
|
| 15 |
SUBSCRIPTIONS_FILE = 'subscriptions.json'
|
| 16 |
+
# Путь к файлу для хранения VAPID ключей
|
| 17 |
+
VAPID_KEYS_FILE = 'vapid_keys.json'
|
| 18 |
+
|
| 19 |
+
# --- Генерация и загрузка VAPID ключей ---
|
| 20 |
+
|
| 21 |
+
def generate_vapid_keys():
|
| 22 |
+
"""Генерирует VAPID ключи и возвращает их в формате base64."""
|
| 23 |
+
private_key = ec.generate_private_key(ec.SECP256R1())
|
| 24 |
+
public_key = private_key.public_key()
|
| 25 |
+
|
| 26 |
+
private_pem = private_key.private_bytes(
|
| 27 |
+
encoding=serialization.Encoding.PEM,
|
| 28 |
+
format=serialization.PrivateFormat.PKCS8,
|
| 29 |
+
encryption_algorithm=serialization.NoEncryption()
|
| 30 |
+
)
|
| 31 |
+
public_pem = public_key.public_bytes(
|
| 32 |
+
encoding=serialization.Encoding.PEM,
|
| 33 |
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
private_b64 = urlsafe_b64encode(private_pem).decode('utf-8').rstrip('=')
|
| 37 |
+
public_b64 = urlsafe_b64encode(public_pem).decode('utf-8').rstrip('=')
|
| 38 |
+
|
| 39 |
+
return {"private_key": private_b64, "public_key": public_b64}
|
| 40 |
+
|
| 41 |
+
def load_or_generate_vapid_keys():
|
| 42 |
+
"""Загружает VAPID ключи из файла или генерирует новые, если файла нет."""
|
| 43 |
+
if os.path.exists(VAPID_KEYS_FILE):
|
| 44 |
+
with open(VAPID_KEYS_FILE, 'r') as f:
|
| 45 |
+
return json.load(f)
|
| 46 |
+
else:
|
| 47 |
+
keys = generate_vapid_keys()
|
| 48 |
+
with open(VAPID_KEYS_FILE, 'w') as f:
|
| 49 |
+
json.dump(keys, f, indent=4)
|
| 50 |
+
print(f"Сгенерированы новые VAPID ключи и сохранены в {VAPID_KEYS_FILE}")
|
| 51 |
+
return keys
|
| 52 |
+
|
| 53 |
+
# Загружаем или генерируем ключи при старте приложения
|
| 54 |
+
vapid_keys = load_or_generate_vapid_keys()
|
| 55 |
+
VAPID_PUBLIC_KEY_BASE64 = vapid_keys["public_key"]
|
| 56 |
+
VAPID_PRIVATE_KEY_BASE64 = vapid_keys["private_key"]
|
| 57 |
+
VAPID_CLAIMS = {"subject": "mailto:your-email@example.com"} # Замените на свой email
|
| 58 |
|
| 59 |
# --- Вспомогательные функции ---
|
| 60 |
|
|
|
|
| 87 |
def send_push_notification(subscription, message_body):
|
| 88 |
"""Отправляет push уведомление одному подписчику."""
|
| 89 |
try:
|
| 90 |
+
print(f"Отправка уведомления на: {subscription['endpoint']}")
|
| 91 |
webpush(
|
| 92 |
subscription_info=subscription,
|
| 93 |
data=message_body,
|
|
|
|
| 95 |
vapid_public_key=VAPID_PUBLIC_KEY_BASE64,
|
| 96 |
vapid_claims=VAPID_CLAIMS
|
| 97 |
)
|
| 98 |
+
print(f"Уведомление успешно отправлено на: {subscription['endpoint']}")
|
| 99 |
return True
|
| 100 |
+
except WebPushException as e:
|
| 101 |
+
print(f"Ошибка отправки уведомления на: {subscription['endpoint']}. Ошибка: {e}")
|
| 102 |
return False
|
| 103 |
|
| 104 |
def send_push_to_all(message_body):
|
|
|
|
| 109 |
if send_push_notification(sub, message_body):
|
| 110 |
valid_subscriptions.append(sub)
|
| 111 |
else:
|
| 112 |
+
print(f"Подписка удалена из-за оши��ки: {sub['endpoint']}")
|
| 113 |
+
if valid_subscriptions != subscriptions:
|
| 114 |
+
save_subscriptions(valid_subscriptions)
|
| 115 |
+
return len(valid_subscriptions) > 0
|
| 116 |
|
| 117 |
# --- HTML для главной страницы ---
|
| 118 |
HTML_INDEX = """
|
|
|
|
| 166 |
Notification.requestPermission().then(permission => {
|
| 167 |
if (permission === "granted") {
|
| 168 |
console.log("Разрешение на уведомления получено.");
|
| 169 |
+
subscribeUserToPush();
|
| 170 |
} else {
|
| 171 |
console.log("Разрешение на уведомления отклонено.");
|
| 172 |
}
|
| 173 |
});
|
| 174 |
} else if (Notification.permission === "granted") {
|
| 175 |
console.log("Разрешение на уведомления уже есть.");
|
| 176 |
+
subscribeUserToPush();
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
}
|
| 179 |
|
|
|
|
| 193 |
navigator.serviceWorker.ready.then(serviceWorkerRegistration => {
|
| 194 |
serviceWorkerRegistration.pushManager.subscribe({
|
| 195 |
userVisibleOnly: true,
|
| 196 |
+
applicationServerKey: urlBase64ToUint8Array(publicKey)
|
| 197 |
})
|
| 198 |
.then(subscription => {
|
| 199 |
+
console.log('Успешная подписка на Push API:', subscription);
|
| 200 |
sendSubscriptionToServer(subscription);
|
| 201 |
})
|
| 202 |
.catch(error => {
|
|
|
|
| 220 |
return response.json();
|
| 221 |
})
|
| 222 |
.then(data => {
|
| 223 |
+
console.log('Подписка успешно сохранена:', data.message);
|
| 224 |
})
|
| 225 |
.catch(error => {
|
| 226 |
+
console.error('Ошибка при сохранении подписки:', error);
|
| 227 |
});
|
| 228 |
}
|
| 229 |
|
|
|
|
| 232 |
.then(response => response.json())
|
| 233 |
.then(news => {
|
| 234 |
const newsList = document.getElementById('news-list');
|
| 235 |
+
newsList.innerHTML = '';
|
| 236 |
news.forEach(item => {
|
| 237 |
const li = document.createElement('li');
|
| 238 |
li.className = 'list-group-item';
|
| 239 |
li.textContent = item.text;
|
| 240 |
newsList.appendChild(li);
|
| 241 |
});
|
| 242 |
+
})
|
| 243 |
+
.catch(error => {
|
| 244 |
+
console.error('Ошибка загрузки новостей:', error);
|
| 245 |
});
|
| 246 |
}
|
| 247 |
|
|
|
|
| 259 |
.then(response => response.json())
|
| 260 |
.then(data => {
|
| 261 |
console.log(data.message);
|
| 262 |
+
document.getElementById('news-text').value = '';
|
| 263 |
+
loadNews();
|
| 264 |
+
})
|
| 265 |
+
.catch(error => {
|
| 266 |
+
console.error('Ошибка добавления новости:', error);
|
| 267 |
});
|
| 268 |
}
|
| 269 |
}
|
| 270 |
|
|
|
|
| 271 |
function urlBase64ToUint8Array(base64String) {
|
| 272 |
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
| 273 |
const base64 = (base64String + padding)
|
| 274 |
.replace(/-/g, '+')
|
| 275 |
.replace(/_/g, '/');
|
|
|
|
| 276 |
const rawData = window.atob(base64);
|
| 277 |
const outputArray = new Uint8Array(rawData.length);
|
|
|
|
| 278 |
for (let i = 0; i < rawData.length; ++i) {
|
| 279 |
outputArray[i] = rawData.charCodeAt(i);
|
| 280 |
}
|
| 281 |
return outputArray;
|
| 282 |
}
|
| 283 |
|
|
|
|
|
|
|
| 284 |
navigator.serviceWorker.addEventListener('message', event => {
|
| 285 |
if (event.data && event.data.type === 'push-received') {
|
| 286 |
+
console.log('Push уведомление в foreground:', event.data.message);
|
| 287 |
+
loadNews();
|
|
|
|
| 288 |
}
|
| 289 |
});
|
|
|
|
|
|
|
| 290 |
</script>
|
| 291 |
</body>
|
| 292 |
</html>
|
|
|
|
| 299 |
const title = 'Новая новость!';
|
| 300 |
const options = {
|
| 301 |
body: message,
|
| 302 |
+
icon: '/no-icon.png' // Убедитесь, что иконка существует, или удалите эту строку
|
| 303 |
};
|
| 304 |
|
| 305 |
event.waitUntil(self.registration.showNotification(title, options));
|
| 306 |
|
|
|
|
| 307 |
self.clients.matchAll({ type: 'window' }).then(clients => {
|
| 308 |
clients.forEach(client => {
|
| 309 |
client.postMessage({ type: 'push-received', message: message });
|
|
|
|
| 313 |
|
| 314 |
self.addEventListener('notificationclick', event => {
|
| 315 |
event.notification.close();
|
| 316 |
+
event.waitUntil(clients.openWindow('/'));
|
| 317 |
});
|
| 318 |
"""
|
| 319 |
|
|
|
|
| 329 |
}
|
| 330 |
"""
|
| 331 |
|
|
|
|
| 332 |
# --- Flask маршруты ---
|
| 333 |
|
| 334 |
@app.route('/')
|
| 335 |
def index():
|
| 336 |
+
"""Главная страница."""
|
| 337 |
news = load_news()
|
| 338 |
+
public_key = VAPID_PUBLIC_KEY_BASE64
|
| 339 |
html = HTML_INDEX.replace('%PUBLIC_KEY%', public_key)
|
|
|
|
| 340 |
news_list_html = ''.join([f'<li class="list-group-item">{item["text"]}</li>' for item in news])
|
| 341 |
html = html.replace('<ul class="list-group" id="news-list"></ul>', f'<ul class="list-group" id="news-list">{news_list_html}</ul>')
|
| 342 |
return html
|
| 343 |
|
| 344 |
@app.route('/add_news', methods=['POST'])
|
| 345 |
def add_news():
|
| 346 |
+
"""API endpoint для добавления новости."""
|
| 347 |
+
print("Маршрут /add_news вызван!")
|
| 348 |
+
news_text = request.form.get('news_text')
|
| 349 |
+
if news_text:
|
| 350 |
+
news_list = load_news()
|
| 351 |
+
news_list.append({"text": news_text})
|
| 352 |
+
save_news(news_list)
|
| 353 |
+
success = send_push_to_all(news_text)
|
| 354 |
+
if success:
|
|
|
|
|
|
|
| 355 |
return jsonify({"message": "Новость добавлена и уведомления отправлены"}), 200
|
| 356 |
+
else:
|
| 357 |
+
return jsonify({"message": "Новость добавлена, но уведомления не отправлены"}), 200
|
| 358 |
+
return jsonify({"error": "Текст новости не предоставлен"}), 400
|
| 359 |
|
| 360 |
@app.route('/get_news')
|
| 361 |
def get_news():
|
| 362 |
+
"""API endpoint для получения новостей."""
|
| 363 |
news = load_news()
|
| 364 |
return jsonify(news)
|
| 365 |
|
| 366 |
@app.route('/save_subscription', methods=['POST'])
|
| 367 |
def save_subscription():
|
| 368 |
+
"""API endpoint для сохранения подписки."""
|
| 369 |
subscription_data = request.get_json()
|
| 370 |
if subscription_data:
|
| 371 |
subscriptions = load_subscriptions()
|
| 372 |
+
if not any(sub['endpoint'] == subscription_data['endpoint'] for sub in subscriptions):
|
| 373 |
+
subscriptions.append(subscription_data)
|
| 374 |
+
save_subscriptions(subscriptions)
|
| 375 |
+
print(f"Подписка сохранена: {subscription_data['endpoint']}")
|
| 376 |
return jsonify({"message": "Подписка сохранена"}), 201
|
| 377 |
return jsonify({"error": "Неверные данные подписки"}), 400
|
| 378 |
|
|
|
|
| 386 |
"""Маршрут для manifest.json."""
|
| 387 |
return Response(MANIFEST_JSON, mimetype='application/json')
|
| 388 |
|
|
|
|
| 389 |
if __name__ == '__main__':
|
| 390 |
+
app.run(debug=True, port=PORT, host='0.0.0.0')
|