Kgshop commited on
Commit
ce3c49f
·
verified ·
1 Parent(s): af073a7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +96 -61
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
- # VAPID keys для push уведомлений. **ЗАМЕНИТЕ ЭТИ PLACEHOLDER КЛЮЧИ НА ВАШИ СОБСТВЕННЫЕ!**
14
- VAPID_PUBLIC_KEY_BASE64 = "BIdP1w9i3m7C-p9tQ_97z7LgusQ49t8wZ6-L2vcl1kYRt6b-5Zg848p0h7FvL3Qv8K4j_J9z_n-J9y8v6v918" # **ЗАМЕНИТЬ!**
15
- VAPID_PRIVATE_KEY_BASE64 = "w10j9y0z8x7c6v5b4n3m2l1k0j9h8g7f6e5d4s3a2q1z" # **ЗАМЕНИТЬ!**
16
- VAPID_CLAIMS = {"subject": "mailto:your-email@example.com"} # Замените на свой email
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"Попытка отправить уведомление на endpoint: {subscription['endpoint']}") # Лог перед отправкой
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"Уведомление успешно отправлено на endpoint: {subscription['endpoint']}") # Лог после успешной отправки
58
  return True
59
- except Exception as e:
60
- print(f"Ошибка отправки уведомления на endpoint: {subscription['endpoint']}. Ошибка: {e}") # Лог ошибки
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"Не удалось отправить уведомление для подписки: {sub['endpoint']}") # Логирование для отладки
72
- save_subscriptions(valid_subscriptions) # Сохраняем только валидные подписки
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('Подписка на Push API:', subscription);
160
  sendSubscriptionToServer(subscription);
161
  })
162
  .catch(error => {
@@ -180,10 +220,10 @@ HTML_INDEX = """
180
  return response.json();
181
  })
182
  .then(data => {
183
- console.log('Подписка успешно сохранена на сервере:', data.message);
184
  })
185
  .catch(error => {
186
- console.error('Ошибка при сохранении подписки на сервере:', 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('Получено push уведомление в foreground:', event.data.message);
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
- print("Маршрут /add_news вызван!") # <--- Отладочный вывод (добавлено)
313
- news_text = request.form.get('news_text')
314
- if news_text:
315
- news_list = load_news()
316
- news_list.append({"text": news_text})
317
- save_news(news_list)
318
-
319
- # Отправляем push уведомления о новой новости
320
- send_push_to_all(news_text)
321
-
322
  return jsonify({"message": "Новость добавлена и уведомления отправлены"}), 200
323
- return jsonify({"error": "Текст новости не предоставлен"}), 400
 
 
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
- subscriptions.append(subscription_data)
338
- save_subscriptions(subscriptions)
339
- print(f"Подписка сохранена: {subscription_data['endpoint']}") # Логирование для отладки
 
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') # Доступно извне и на порту 7860
 
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')