Добавить диагностику PWA/Push и endpoint тестового push
This commit is contained in:
parent
185ba5b1d3
commit
d07602b0a9
@ -16,6 +16,7 @@ self.addEventListener('push', (event) => {
|
|||||||
let rawText = '';
|
let rawText = '';
|
||||||
let kind = '';
|
let kind = '';
|
||||||
let fromLogin = '';
|
let fromLogin = '';
|
||||||
|
let title = '';
|
||||||
try {
|
try {
|
||||||
if (event.data) {
|
if (event.data) {
|
||||||
const text = event.data.text();
|
const text = event.data.text();
|
||||||
@ -23,6 +24,7 @@ self.addEventListener('push', (event) => {
|
|||||||
try {
|
try {
|
||||||
const json = JSON.parse(rawText || '{}');
|
const json = JSON.parse(rawText || '{}');
|
||||||
kind = String(json.kind || '');
|
kind = String(json.kind || '');
|
||||||
|
title = String(json.title || '');
|
||||||
body = String(json.text || '');
|
body = String(json.text || '');
|
||||||
fromLogin = String(json.fromLogin || '');
|
fromLogin = String(json.fromLogin || '');
|
||||||
} catch {
|
} catch {
|
||||||
@ -33,11 +35,14 @@ self.addEventListener('push', (event) => {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldNotify = kind === 'new_message' || (!kind && body);
|
const shouldNotify = kind === 'new_message' || kind === 'test_push' || (!kind && body);
|
||||||
|
const notificationTitle = kind === 'test_push'
|
||||||
|
? (title || 'SHiNE: тестовый push')
|
||||||
|
: 'SHiNE: входящее сообщение';
|
||||||
const notifyPromise = shouldNotify
|
const notifyPromise = shouldNotify
|
||||||
? self.registration.showNotification('SHiNE: входящее сообщение', {
|
? self.registration.showNotification(notificationTitle, {
|
||||||
body: body || (fromLogin ? `Вам пришло сообщение от ${fromLogin}` : 'Вам пришло сообщение'),
|
body: body || (fromLogin ? `Вам пришло сообщение от ${fromLogin}` : 'Вам пришло сообщение'),
|
||||||
tag: 'shine-direct-message',
|
tag: kind === 'test_push' ? 'shine-test-push' : 'shine-direct-message',
|
||||||
renotify: true,
|
renotify: true,
|
||||||
})
|
})
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import * as showKeysView from './pages/show-keys-view.js';
|
|||||||
import * as deviceSessionView from './pages/device-session-view.js';
|
import * as deviceSessionView from './pages/device-session-view.js';
|
||||||
import * as languageView from './pages/language-view.js';
|
import * as languageView from './pages/language-view.js';
|
||||||
import * as appLogView from './pages/app-log-view.js';
|
import * as appLogView from './pages/app-log-view.js';
|
||||||
|
import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js';
|
||||||
import * as messagesList from './pages/messages-list.js';
|
import * as messagesList from './pages/messages-list.js';
|
||||||
import * as contactSearchView from './pages/contact-search-view.js';
|
import * as contactSearchView from './pages/contact-search-view.js';
|
||||||
import * as chatView from './pages/chat-view.js';
|
import * as chatView from './pages/chat-view.js';
|
||||||
@ -78,6 +79,7 @@ const routes = {
|
|||||||
'device-session-view': deviceSessionView,
|
'device-session-view': deviceSessionView,
|
||||||
'language-view': languageView,
|
'language-view': languageView,
|
||||||
'app-log-view': appLogView,
|
'app-log-view': appLogView,
|
||||||
|
'pwa-diagnostics-view': pwaDiagnosticsView,
|
||||||
'messages-list': messagesList,
|
'messages-list': messagesList,
|
||||||
'contact-search-view': contactSearchView,
|
'contact-search-view': contactSearchView,
|
||||||
'chat-view': chatView,
|
'chat-view': chatView,
|
||||||
@ -330,12 +332,25 @@ async function init() {
|
|||||||
const data = event?.data || {};
|
const data = event?.data || {};
|
||||||
if (data.type !== 'SHINE_WEB_PUSH_EVENT') return;
|
if (data.type !== 'SHINE_WEB_PUSH_EVENT') return;
|
||||||
const payload = data.payload || {};
|
const payload = data.payload || {};
|
||||||
|
const kind = String(payload.kind || '').trim();
|
||||||
|
const now = Date.now();
|
||||||
|
try {
|
||||||
|
localStorage.setItem('shine-ui-last-push-at-v1', String(now));
|
||||||
|
localStorage.setItem('shine-ui-last-push-payload-v1', JSON.stringify(payload || {}));
|
||||||
|
if (kind === 'test_push') {
|
||||||
|
localStorage.setItem('shine-ui-last-test-push-status-v1', 'доставлен');
|
||||||
|
localStorage.setItem('shine-ui-last-test-push-at-v1', String(now));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore localStorage errors
|
||||||
|
}
|
||||||
addAppLogEntry({
|
addAppLogEntry({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
source: 'web-push',
|
source: 'web-push',
|
||||||
message: 'Получено push-событие в service worker',
|
message: 'Получено push-событие в service worker',
|
||||||
details: payload,
|
details: payload,
|
||||||
});
|
});
|
||||||
|
window.dispatchEvent(new CustomEvent('shine-push-diagnostics-update', { detail: payload }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
533
shine-UI/js/pages/pwa-diagnostics-view.js
Normal file
533
shine-UI/js/pages/pwa-diagnostics-view.js
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
import { renderHeader } from '../components/header.js';
|
||||||
|
import { authService, state } from '../state.js';
|
||||||
|
import { canInstallPwa, isStandalonePwaMode } from '../services/pwa-install-service.js';
|
||||||
|
|
||||||
|
export const pageMeta = { id: 'pwa-diagnostics-view', title: 'Диагностика PWA / Push' };
|
||||||
|
|
||||||
|
const LS_SUBSCRIPTION_KEY = 'shine-ui-webpush-subscription-v1';
|
||||||
|
const LS_SUBSCRIPTION_UPDATED_AT_KEY = 'shine-ui-webpush-subscription-updated-at-v1';
|
||||||
|
const LS_LAST_TEST_STATUS_KEY = 'shine-ui-last-test-push-status-v1';
|
||||||
|
const LS_LAST_TEST_AT_KEY = 'shine-ui-last-test-push-at-v1';
|
||||||
|
const LS_LAST_PUSH_PAYLOAD_KEY = 'shine-ui-last-push-payload-v1';
|
||||||
|
|
||||||
|
function vapidBase64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
for (let i = 0; i < rawData.length; i += 1) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTs(ts) {
|
||||||
|
const num = Number(ts);
|
||||||
|
if (!Number.isFinite(num) || num <= 0) return '—';
|
||||||
|
try {
|
||||||
|
return new Date(num).toLocaleString('ru-RU');
|
||||||
|
} catch {
|
||||||
|
return String(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectLaunchMode() {
|
||||||
|
try {
|
||||||
|
if (window.matchMedia?.('(display-mode: standalone)')?.matches || window.navigator?.standalone === true) {
|
||||||
|
return 'standalone';
|
||||||
|
}
|
||||||
|
if (window.matchMedia?.('(display-mode: fullscreen)')?.matches) {
|
||||||
|
return 'fullscreen';
|
||||||
|
}
|
||||||
|
if (window.matchMedia?.('(display-mode: browser)')?.matches) {
|
||||||
|
return 'браузер';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
} catch {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectIosSafari() {
|
||||||
|
const ua = String(navigator.userAgent || '').toLowerCase();
|
||||||
|
const isIos = /iphone|ipad|ipod/.test(ua);
|
||||||
|
const isSafari = /safari/.test(ua) && !/crios|fxios|edgios|opr\//.test(ua);
|
||||||
|
return isIos && isSafari;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInstallState({ launchMode, manifestOk, swRegistered }) {
|
||||||
|
if (launchMode === 'standalone' || launchMode === 'fullscreen') return 'уже установлено';
|
||||||
|
if (canInstallPwa()) return 'доступна кнопка установки';
|
||||||
|
if (!window.isSecureContext || !manifestOk || !swRegistered) return 'недоступно';
|
||||||
|
return 'браузер не сообщает';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toneFor(value, type = 'binary') {
|
||||||
|
const v = String(value || '').toLowerCase();
|
||||||
|
if (type === 'permission') {
|
||||||
|
if (v === 'granted') return 'ok';
|
||||||
|
if (v === 'default') return 'warn';
|
||||||
|
if (v === 'denied') return 'bad';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
if (type === 'testPush') {
|
||||||
|
if (v === 'доставлен') return 'ok';
|
||||||
|
if (v === 'отправлен') return 'warn';
|
||||||
|
if (v === 'ошибка') return 'bad';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
if (type === 'install') {
|
||||||
|
if (v === 'уже установлено' || v === 'доступна кнопка установки') return 'ok';
|
||||||
|
if (v === 'браузер не сообщает' || v === 'недоступно') return 'warn';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
if (type === 'launchMode') {
|
||||||
|
if (v === 'standalone' || v === 'fullscreen' || v === 'браузер') return 'ok';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
if (v === 'да' || v === 'найден' || v === 'зарегистрирован' || v === 'поддерживается' || v === 'есть') return 'ok';
|
||||||
|
if (v === 'не найден' || v === 'не зарегистрирован' || v === 'нет' || v === 'ошибка') return 'bad';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowEl(label, value, tone) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'pwa-diag-row';
|
||||||
|
|
||||||
|
const left = document.createElement('span');
|
||||||
|
left.className = 'pwa-diag-key';
|
||||||
|
left.textContent = label;
|
||||||
|
|
||||||
|
const right = document.createElement('span');
|
||||||
|
right.className = 'pwa-diag-value';
|
||||||
|
right.textContent = String(value || '—');
|
||||||
|
|
||||||
|
const indicator = document.createElement('span');
|
||||||
|
indicator.className = `pwa-diag-indicator is-${tone || 'neutral'}`;
|
||||||
|
|
||||||
|
row.append(left, right, indicator);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectDiagnostics() {
|
||||||
|
const result = {
|
||||||
|
secureContext: window.isSecureContext === true,
|
||||||
|
manifestStatus: 'не найден',
|
||||||
|
manifestUrl: '',
|
||||||
|
swStatus: 'не зарегистрирован',
|
||||||
|
swError: '',
|
||||||
|
launchMode: detectLaunchMode(),
|
||||||
|
installAvailability: 'браузер не сообщает',
|
||||||
|
notificationsApi: 'нет',
|
||||||
|
pushApi: 'нет',
|
||||||
|
notificationPermission: 'default',
|
||||||
|
pushSubscription: 'нет',
|
||||||
|
subscriptionEndpoint: '',
|
||||||
|
subscriptionUpdatedAtMs: 0,
|
||||||
|
lastTestPush: 'не запускался',
|
||||||
|
lastTestPushAtMs: 0,
|
||||||
|
lastPushPayload: null,
|
||||||
|
iosSafariHint: false,
|
||||||
|
tech: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
result.notificationsApi = 'Notification' in window ? 'поддерживается' : 'нет';
|
||||||
|
result.pushApi = 'PushManager' in window ? 'поддерживается' : 'нет';
|
||||||
|
if ('Notification' in window) {
|
||||||
|
result.notificationPermission = String(Notification.permission || 'default');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const linkEl = document.querySelector('link[rel="manifest"]');
|
||||||
|
const href = String(linkEl?.getAttribute('href') || '').trim();
|
||||||
|
if (href) {
|
||||||
|
const url = new URL(href, window.location.href).toString();
|
||||||
|
const resp = await fetch(url, { cache: 'no-store' });
|
||||||
|
result.manifestStatus = resp.ok ? 'найден' : 'не найден';
|
||||||
|
result.manifestUrl = url;
|
||||||
|
} else {
|
||||||
|
result.manifestStatus = 'не найден';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
result.manifestStatus = 'не найден';
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration = null;
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
result.swStatus = registration ? 'зарегистрирован' : 'не зарегистрирован';
|
||||||
|
} catch (error) {
|
||||||
|
result.swStatus = 'ошибка';
|
||||||
|
result.swError = error?.message || 'unknown';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.swStatus = 'не зарегистрирован';
|
||||||
|
}
|
||||||
|
|
||||||
|
result.installAvailability = buildInstallState({
|
||||||
|
launchMode: result.launchMode,
|
||||||
|
manifestOk: result.manifestStatus === 'найден',
|
||||||
|
swRegistered: result.swStatus === 'зарегистрирован',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (registration && ('PushManager' in window)) {
|
||||||
|
try {
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
if (subscription) {
|
||||||
|
result.pushSubscription = 'есть';
|
||||||
|
result.subscriptionEndpoint = String(subscription.endpoint || '');
|
||||||
|
} else {
|
||||||
|
result.pushSubscription = 'нет';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
result.pushSubscription = 'ошибка получения';
|
||||||
|
result.swError = result.swError || (error?.message || 'unknown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedAt = Number(localStorage.getItem(LS_SUBSCRIPTION_UPDATED_AT_KEY) || 0);
|
||||||
|
result.subscriptionUpdatedAtMs = Number.isFinite(updatedAt) ? updatedAt : 0;
|
||||||
|
} catch {
|
||||||
|
result.subscriptionUpdatedAtMs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const st = String(localStorage.getItem(LS_LAST_TEST_STATUS_KEY) || '').trim();
|
||||||
|
const at = Number(localStorage.getItem(LS_LAST_TEST_AT_KEY) || 0);
|
||||||
|
if (st) result.lastTestPush = st;
|
||||||
|
if (Number.isFinite(at) && at > 0) result.lastTestPushAtMs = at;
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LS_LAST_PUSH_PAYLOAD_KEY);
|
||||||
|
if (raw) result.lastPushPayload = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
result.lastPushPayload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.iosSafariHint = detectIosSafari() && !isStandalonePwaMode();
|
||||||
|
|
||||||
|
result.tech = {
|
||||||
|
secureContext: window.isSecureContext,
|
||||||
|
hasServiceWorkerApi: 'serviceWorker' in navigator,
|
||||||
|
hasPushManagerApi: 'PushManager' in window,
|
||||||
|
hasNotificationsApi: 'Notification' in window,
|
||||||
|
notificationPermission: result.notificationPermission,
|
||||||
|
launchMode: result.launchMode,
|
||||||
|
canInstallPrompt: canInstallPwa(),
|
||||||
|
swStatus: result.swStatus,
|
||||||
|
swError: result.swError,
|
||||||
|
manifestStatus: result.manifestStatus,
|
||||||
|
manifestUrl: result.manifestUrl,
|
||||||
|
pushSubscription: result.pushSubscription,
|
||||||
|
subscriptionEndpoint: result.subscriptionEndpoint,
|
||||||
|
subscriptionUpdatedAtMs: result.subscriptionUpdatedAtMs,
|
||||||
|
lastTestPush: result.lastTestPush,
|
||||||
|
lastTestPushAtMs: result.lastTestPushAtMs,
|
||||||
|
lastPushPayload: result.lastPushPayload,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRecommendations(diag) {
|
||||||
|
const rows = [];
|
||||||
|
if (!diag.secureContext) {
|
||||||
|
rows.push('Откройте приложение через HTTPS. Без этого push и install-mode работают нестабильно.');
|
||||||
|
}
|
||||||
|
if (diag.manifestStatus !== 'найден') {
|
||||||
|
rows.push('Проверьте доступность `manifest.webmanifest` и заголовки кэша для него.');
|
||||||
|
}
|
||||||
|
if (diag.swStatus !== 'зарегистрирован') {
|
||||||
|
rows.push('Service Worker не зарегистрирован. Проверьте путь `firebase-messaging-sw.js` и HTTPS.');
|
||||||
|
}
|
||||||
|
if (diag.notificationPermission === 'default') {
|
||||||
|
rows.push('Выдайте разрешение на уведомления через кнопку «Запросить разрешение на уведомления».');
|
||||||
|
} else if (diag.notificationPermission === 'denied') {
|
||||||
|
rows.push('Разрешение уведомлений заблокировано в браузере. Разрешите уведомления в настройках сайта.');
|
||||||
|
}
|
||||||
|
if (diag.pushSubscription !== 'есть') {
|
||||||
|
rows.push('Создайте push-подписку кнопкой «Подписаться на push».');
|
||||||
|
}
|
||||||
|
if (diag.installAvailability === 'доступна кнопка установки') {
|
||||||
|
rows.push('Установите приложение как PWA для более стабильной доставки push в фоне.');
|
||||||
|
}
|
||||||
|
if (diag.iosSafariHint) {
|
||||||
|
rows.push('iPhone/Safari: push работает только после «Добавить на экран Домой».');
|
||||||
|
}
|
||||||
|
if (!rows.length) {
|
||||||
|
rows.push('Критических проблем не найдено. Проверьте тестовый push для финальной валидации.');
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render({ navigate }) {
|
||||||
|
const screen = document.createElement('section');
|
||||||
|
screen.className = 'stack';
|
||||||
|
|
||||||
|
screen.append(
|
||||||
|
renderHeader({
|
||||||
|
title: 'Диагностика PWA / Push',
|
||||||
|
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusCard = document.createElement('div');
|
||||||
|
statusCard.className = 'card stack';
|
||||||
|
const statusList = document.createElement('div');
|
||||||
|
statusList.className = 'stack pwa-diag-list';
|
||||||
|
statusCard.append(statusList);
|
||||||
|
|
||||||
|
const actionsCard = document.createElement('div');
|
||||||
|
actionsCard.className = 'card stack';
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'pwa-diag-actions';
|
||||||
|
actions.innerHTML = `
|
||||||
|
<button class="text-btn" type="button" id="diag-refresh">Проверить заново</button>
|
||||||
|
<button class="text-btn" type="button" id="diag-notif">Запросить разрешение на уведомления</button>
|
||||||
|
<button class="text-btn" type="button" id="diag-subscribe">Подписаться на push</button>
|
||||||
|
<button class="text-btn" type="button" id="diag-unsubscribe">Отписаться от push</button>
|
||||||
|
<button class="text-btn" type="button" id="diag-testpush">Отправить тестовый push</button>
|
||||||
|
<button class="text-btn" type="button" id="diag-toggle-details">Показать тех. детали</button>
|
||||||
|
`;
|
||||||
|
actionsCard.append(actions);
|
||||||
|
|
||||||
|
const recommendationsCard = document.createElement('div');
|
||||||
|
recommendationsCard.className = 'card stack';
|
||||||
|
const recommendationsTitle = document.createElement('strong');
|
||||||
|
recommendationsTitle.textContent = 'Рекомендации';
|
||||||
|
const recommendationsList = document.createElement('div');
|
||||||
|
recommendationsList.className = 'stack pwa-diag-recommendations';
|
||||||
|
recommendationsCard.append(recommendationsTitle, recommendationsList);
|
||||||
|
|
||||||
|
const detailsCard = document.createElement('div');
|
||||||
|
detailsCard.className = 'card stack';
|
||||||
|
detailsCard.hidden = true;
|
||||||
|
const detailsTitle = document.createElement('strong');
|
||||||
|
detailsTitle.textContent = 'Технические детали';
|
||||||
|
const detailsPre = document.createElement('pre');
|
||||||
|
detailsPre.className = 'pwa-diag-json';
|
||||||
|
detailsCard.append(detailsTitle, detailsPre);
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.className = 'meta-muted';
|
||||||
|
|
||||||
|
screen.append(statusCard, actionsCard, recommendationsCard, detailsCard, info);
|
||||||
|
|
||||||
|
let latestDiag = null;
|
||||||
|
let lastTestResponse = null;
|
||||||
|
|
||||||
|
const setInfo = (text) => {
|
||||||
|
info.textContent = String(text || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const rerender = () => {
|
||||||
|
if (!latestDiag) return;
|
||||||
|
statusList.innerHTML = '';
|
||||||
|
|
||||||
|
statusList.append(
|
||||||
|
rowEl('HTTPS', latestDiag.secureContext ? 'да' : 'нет', toneFor(latestDiag.secureContext ? 'да' : 'нет')),
|
||||||
|
rowEl('Manifest', latestDiag.manifestStatus, toneFor(latestDiag.manifestStatus)),
|
||||||
|
rowEl('Service Worker', latestDiag.swStatus, toneFor(latestDiag.swStatus)),
|
||||||
|
rowEl('Режим запуска', latestDiag.launchMode, toneFor(latestDiag.launchMode, 'launchMode')),
|
||||||
|
rowEl('Возможность установки', latestDiag.installAvailability, toneFor(latestDiag.installAvailability, 'install')),
|
||||||
|
rowEl('Notifications API', latestDiag.notificationsApi, toneFor(latestDiag.notificationsApi)),
|
||||||
|
rowEl('Push API', latestDiag.pushApi, toneFor(latestDiag.pushApi)),
|
||||||
|
rowEl('Разрешение на уведомления', latestDiag.notificationPermission, toneFor(latestDiag.notificationPermission, 'permission')),
|
||||||
|
rowEl('Push subscription', latestDiag.pushSubscription, toneFor(latestDiag.pushSubscription)),
|
||||||
|
rowEl('Последний тест push', latestDiag.lastTestPush, toneFor(latestDiag.lastTestPush, 'testPush')),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (latestDiag.subscriptionEndpoint) {
|
||||||
|
statusList.append(rowEl('Endpoint подписки', latestDiag.subscriptionEndpoint, 'neutral'));
|
||||||
|
}
|
||||||
|
if (latestDiag.subscriptionUpdatedAtMs) {
|
||||||
|
statusList.append(rowEl('Дата обновления подписки', formatTs(latestDiag.subscriptionUpdatedAtMs), 'neutral'));
|
||||||
|
}
|
||||||
|
if (latestDiag.lastTestPushAtMs) {
|
||||||
|
statusList.append(rowEl('Время последнего теста', formatTs(latestDiag.lastTestPushAtMs), 'neutral'));
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendationsList.innerHTML = '';
|
||||||
|
buildRecommendations(latestDiag).forEach((entry) => {
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'meta-muted';
|
||||||
|
p.textContent = `• ${entry}`;
|
||||||
|
recommendationsList.append(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
const details = {
|
||||||
|
diagnostics: latestDiag.tech || {},
|
||||||
|
lastTestPushResponse: lastTestResponse,
|
||||||
|
login: state.session.login || '',
|
||||||
|
sessionId: state.session.sessionId || '',
|
||||||
|
};
|
||||||
|
detailsPre.textContent = JSON.stringify(details, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadStatuses = async () => {
|
||||||
|
setInfo('Обновляю статусы...');
|
||||||
|
latestDiag = await collectDiagnostics();
|
||||||
|
rerender();
|
||||||
|
if (latestDiag.swError) {
|
||||||
|
setInfo(`Есть предупреждения: ${latestDiag.swError}`);
|
||||||
|
} else {
|
||||||
|
setInfo(`Статусы обновлены: ${new Date().toLocaleTimeString('ru-RU')}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const withButtonBusy = async (btn, fn) => {
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actions.querySelector('#diag-refresh').addEventListener('click', async (event) => {
|
||||||
|
await withButtonBusy(event.currentTarget, reloadStatuses);
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.querySelector('#diag-notif').addEventListener('click', async (event) => {
|
||||||
|
await withButtonBusy(event.currentTarget, async () => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
setInfo('Этот браузер не поддерживает Notifications API.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
setInfo(`Разрешение на уведомления: ${permission}`);
|
||||||
|
await reloadStatuses();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.querySelector('#diag-subscribe').addEventListener('click', async (event) => {
|
||||||
|
await withButtonBusy(event.currentTarget, async () => {
|
||||||
|
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
||||||
|
setInfo('ServiceWorker/PushManager не поддерживаются в этом браузере.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const vapidPublicKey = String(window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__ || '').trim();
|
||||||
|
if (!vapidPublicKey) {
|
||||||
|
setInfo('Не задан публичный VAPID ключ.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.register('./firebase-messaging-sw.js');
|
||||||
|
let permission = String(Notification.permission || 'default');
|
||||||
|
if (permission !== 'granted') {
|
||||||
|
permission = await Notification.requestPermission();
|
||||||
|
}
|
||||||
|
if (permission !== 'granted') {
|
||||||
|
setInfo(`Разрешение на уведомления: ${permission}`);
|
||||||
|
await reloadStatuses();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sub = await registration.pushManager.getSubscription();
|
||||||
|
if (!sub) {
|
||||||
|
sub = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: vapidBase64ToUint8Array(vapidPublicKey),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const json = sub.toJSON();
|
||||||
|
await authService.upsertPushToken({
|
||||||
|
endpoint: json.endpoint || '',
|
||||||
|
p256dhKey: json.keys?.p256dh || '',
|
||||||
|
authKey: json.keys?.auth || '',
|
||||||
|
platform: 'web',
|
||||||
|
userAgent: navigator.userAgent || '',
|
||||||
|
});
|
||||||
|
const now = Date.now();
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LS_SUBSCRIPTION_KEY, JSON.stringify(sub));
|
||||||
|
localStorage.setItem(LS_SUBSCRIPTION_UPDATED_AT_KEY, String(now));
|
||||||
|
} catch {
|
||||||
|
// ignore local storage errors
|
||||||
|
}
|
||||||
|
setInfo('Push-подписка создана и отправлена на сервер.');
|
||||||
|
await reloadStatuses();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.querySelector('#diag-unsubscribe').addEventListener('click', async (event) => {
|
||||||
|
await withButtonBusy(event.currentTarget, async () => {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
setInfo('Service Worker API недоступен.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const registration = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!registration || !registration.pushManager) {
|
||||||
|
setInfo('PushManager недоступен.');
|
||||||
|
await reloadStatuses();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sub = await registration.pushManager.getSubscription();
|
||||||
|
if (!sub) {
|
||||||
|
setInfo('Активная push-подписка не найдена.');
|
||||||
|
await reloadStatuses();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sub.unsubscribe();
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(LS_SUBSCRIPTION_KEY);
|
||||||
|
localStorage.setItem(LS_SUBSCRIPTION_UPDATED_AT_KEY, String(Date.now()));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
setInfo('Push-подписка удалена на клиенте.');
|
||||||
|
await reloadStatuses();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.querySelector('#diag-testpush').addEventListener('click', async (event) => {
|
||||||
|
await withButtonBusy(event.currentTarget, async () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LS_LAST_TEST_STATUS_KEY, 'отправлен');
|
||||||
|
localStorage.setItem(LS_LAST_TEST_AT_KEY, String(Date.now()));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
const resp = await authService.sendTestWebPush({
|
||||||
|
login: state.session.login || '',
|
||||||
|
title: 'SHiNE: тестовый push',
|
||||||
|
text: `Тестовый push для ${state.session.login || 'unknown'}`,
|
||||||
|
});
|
||||||
|
lastTestResponse = resp;
|
||||||
|
setInfo(`Тестовый push отправлен. Доставлено: ${Number(resp?.delivered || 0)}.`);
|
||||||
|
await reloadStatuses();
|
||||||
|
}).catch(async (error) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LS_LAST_TEST_STATUS_KEY, 'ошибка');
|
||||||
|
localStorage.setItem(LS_LAST_TEST_AT_KEY, String(Date.now()));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
setInfo(`Ошибка тестового push: ${error?.message || 'unknown'}`);
|
||||||
|
await reloadStatuses();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.querySelector('#diag-toggle-details').addEventListener('click', (event) => {
|
||||||
|
detailsCard.hidden = !detailsCard.hidden;
|
||||||
|
event.currentTarget.textContent = detailsCard.hidden ? 'Показать тех. детали' : 'Скрыть тех. детали';
|
||||||
|
});
|
||||||
|
|
||||||
|
const onPushDiagUpdate = async () => {
|
||||||
|
await reloadStatuses();
|
||||||
|
};
|
||||||
|
window.addEventListener('shine-push-diagnostics-update', onPushDiagUpdate);
|
||||||
|
|
||||||
|
void reloadStatuses();
|
||||||
|
|
||||||
|
screen.cleanup = () => {
|
||||||
|
window.removeEventListener('shine-push-diagnostics-update', onPushDiagUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ export function render({ navigate }) {
|
|||||||
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
|
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
|
||||||
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
||||||
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
|
<button class="text-btn" type="button" id="settings-app-log">Лог приложения</button>
|
||||||
|
<button class="text-btn" type="button" id="settings-pwa-diagnostics">Диагностика PWA / Push</button>
|
||||||
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
|
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
|
||||||
<button class="text-btn" type="button" id="settings-pwa-install">Зарегистрировать PWA</button>
|
<button class="text-btn" type="button" id="settings-pwa-install">Зарегистрировать PWA</button>
|
||||||
`;
|
`;
|
||||||
@ -36,6 +37,7 @@ export function render({ navigate }) {
|
|||||||
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
|
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
|
||||||
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
||||||
card.querySelector('#settings-app-log').addEventListener('click', () => navigate('app-log-view'));
|
card.querySelector('#settings-app-log').addEventListener('click', () => navigate('app-log-view'));
|
||||||
|
card.querySelector('#settings-pwa-diagnostics').addEventListener('click', () => navigate('pwa-diagnostics-view'));
|
||||||
|
|
||||||
const signOutBtn = card.querySelector('#settings-signout');
|
const signOutBtn = card.querySelector('#settings-signout');
|
||||||
const pwaInstallBtn = card.querySelector('#settings-pwa-install');
|
const pwaInstallBtn = card.querySelector('#settings-pwa-install');
|
||||||
|
|||||||
@ -98,7 +98,8 @@ export function resolveToolbarActive(pageId) {
|
|||||||
pageId === 'show-keys-view' ||
|
pageId === 'show-keys-view' ||
|
||||||
pageId === 'device-session-view' ||
|
pageId === 'device-session-view' ||
|
||||||
pageId === 'language-view' ||
|
pageId === 'language-view' ||
|
||||||
pageId === 'app-log-view'
|
pageId === 'app-log-view' ||
|
||||||
|
pageId === 'pwa-diagnostics-view'
|
||||||
) {
|
) {
|
||||||
return 'profile-view';
|
return 'profile-view';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1226,6 +1226,17 @@ export class AuthService {
|
|||||||
return response.payload || {};
|
return response.payload || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendTestWebPush({ login = '', sessionId = '', title = '', text = '' } = {}) {
|
||||||
|
const payload = {};
|
||||||
|
if (String(login || '').trim()) payload.login = String(login || '').trim();
|
||||||
|
if (String(sessionId || '').trim()) payload.sessionId = String(sessionId || '').trim();
|
||||||
|
if (String(title || '').trim()) payload.title = String(title || '').trim();
|
||||||
|
if (String(text || '').trim()) payload.text = String(text || '').trim();
|
||||||
|
const response = await this.ws.request('SendTestWebPush', payload);
|
||||||
|
if (response.status !== 200) throw opError('SendTestWebPush', response);
|
||||||
|
return response.payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
async buildSignedDm2Block({
|
async buildSignedDm2Block({
|
||||||
login,
|
login,
|
||||||
toLogin,
|
toLogin,
|
||||||
|
|||||||
@ -726,6 +726,82 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pwa-diag-list {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto 10px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(143, 167, 215, 0.18);
|
||||||
|
background: rgba(14, 24, 45, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-key {
|
||||||
|
color: #cdd9f2;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-value {
|
||||||
|
color: #edf3ff;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-indicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #64708a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-indicator.is-ok {
|
||||||
|
background: #66d69a;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 214, 154, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-indicator.is-warn {
|
||||||
|
background: #f0c56b;
|
||||||
|
box-shadow: 0 0 0 3px rgba(240, 197, 107, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-indicator.is-bad {
|
||||||
|
background: #e48792;
|
||||||
|
box-shadow: 0 0 0 3px rgba(228, 135, 146, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-indicator.is-neutral {
|
||||||
|
background: #8994ab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-recommendations {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-diag-json {
|
||||||
|
margin: 0;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #dfe9ff;
|
||||||
|
background: rgba(8, 15, 28, 0.82);
|
||||||
|
border: 1px solid rgba(134, 157, 205, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.contact-search-actions {
|
.contact-search-actions {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@ -65,6 +65,7 @@ import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
|||||||
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_SendTestWebPush_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckIncomingMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckIncomingMessage_Request;
|
||||||
@ -73,6 +74,7 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_R
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
|
||||||
|
|
||||||
// --- NEW: Ping ---
|
// --- NEW: Ping ---
|
||||||
@ -129,6 +131,7 @@ public final class JsonHandlerRegistry {
|
|||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
||||||
|
Map.entry("SendTestWebPush", new Net_SendTestWebPush_Handler()),
|
||||||
Map.entry("SendDirectMessage", new Net_SendDirectMessage_Handler()),
|
Map.entry("SendDirectMessage", new Net_SendDirectMessage_Handler()),
|
||||||
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
||||||
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
||||||
@ -180,6 +183,7 @@ public final class JsonHandlerRegistry {
|
|||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
||||||
|
Map.entry("SendTestWebPush", Net_SendTestWebPush_Request.class),
|
||||||
Map.entry("SendDirectMessage", Net_SendDirectMessage_Request.class),
|
Map.entry("SendDirectMessage", Net_SendDirectMessage_Request.class),
|
||||||
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
||||||
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.push.WebPushSender;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_SendTestWebPush_Handler implements JsonMessageHandler {
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_SendTestWebPush_Request req = (Net_SendTestWebPush_Request) baseRequest;
|
||||||
|
if (ctx == null || !ctx.isAuthenticatedUser()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
|
||||||
|
}
|
||||||
|
|
||||||
|
String authLogin = String.valueOf(ctx.getLogin() == null ? "" : ctx.getLogin()).trim();
|
||||||
|
String targetLogin = String.valueOf(req.getLogin() == null ? "" : req.getLogin()).trim();
|
||||||
|
if (targetLogin.isBlank()) targetLogin = authLogin;
|
||||||
|
|
||||||
|
if (!targetLogin.equalsIgnoreCase(authLogin)) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"FORBIDDEN_TARGET_LOGIN",
|
||||||
|
"Разрешена отправка тестового push только для текущего авторизованного пользователя"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActiveSessionEntry> targets = new ArrayList<>();
|
||||||
|
String targetSessionId = String.valueOf(req.getSessionId() == null ? "" : req.getSessionId()).trim();
|
||||||
|
if (!targetSessionId.isBlank()) {
|
||||||
|
ActiveSessionEntry one = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
|
||||||
|
if (one == null || !targetLogin.equalsIgnoreCase(one.getLogin())) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "SESSION_NOT_FOUND", "Сессия для тестового push не найдена");
|
||||||
|
}
|
||||||
|
targets.add(one);
|
||||||
|
} else {
|
||||||
|
targets = ActiveSessionsDAO.getInstance().getByLogin(targetLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = String.valueOf(req.getTitle() == null ? "" : req.getTitle()).trim();
|
||||||
|
if (title.isBlank()) title = "SHiNE: тестовый push";
|
||||||
|
String text = String.valueOf(req.getText() == null ? "" : req.getText()).trim();
|
||||||
|
if (text.isBlank()) text = "Тестовое push-уведомление отправлено успешно.";
|
||||||
|
|
||||||
|
int sessionsWithPushConfig = 0;
|
||||||
|
int delivered = 0;
|
||||||
|
int failed = 0;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (ActiveSessionEntry s : targets) {
|
||||||
|
if (isBlank(s.getPushEndpoint()) || isBlank(s.getPushP256dhKey()) || isBlank(s.getPushAuthKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sessionsWithPushConfig++;
|
||||||
|
String payload = "{\"kind\":\"test_push\",\"title\":\"" + jsonEscape(title) + "\",\"text\":\"" + jsonEscape(text) + "\",\"sentAt\":" + now + "}";
|
||||||
|
boolean ok = WebPushSender.sendBase64Payload(
|
||||||
|
s.getPushEndpoint(),
|
||||||
|
s.getPushP256dhKey(),
|
||||||
|
s.getPushAuthKey(),
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
if (ok) delivered++;
|
||||||
|
else failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_SendTestWebPush_Response resp = new Net_SendTestWebPush_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setTargetLogin(targetLogin);
|
||||||
|
resp.setAttemptedSessions(targets.size());
|
||||||
|
resp.setSessionsWithPushConfig(sessionsWithPushConfig);
|
||||||
|
resp.setDelivered(delivered);
|
||||||
|
resp.setFailed(failed);
|
||||||
|
resp.setSentAtMs(now);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlank(String s) {
|
||||||
|
return s == null || s.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jsonEscape(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
if (c == '\\') out.append("\\\\");
|
||||||
|
else if (c == '"') out.append("\\\"");
|
||||||
|
else if (c == '\n') out.append("\\n");
|
||||||
|
else if (c == '\r') out.append("\\r");
|
||||||
|
else if (c == '\t') out.append("\\t");
|
||||||
|
else out.append(c);
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_SendTestWebPush_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
private String sessionId;
|
||||||
|
private String title;
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public String getSessionId() { return sessionId; }
|
||||||
|
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getText() { return text; }
|
||||||
|
public void setText(String text) { this.text = text; }
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_SendTestWebPush_Response extends Net_Response {
|
||||||
|
private String targetLogin;
|
||||||
|
private int attemptedSessions;
|
||||||
|
private int sessionsWithPushConfig;
|
||||||
|
private int delivered;
|
||||||
|
private int failed;
|
||||||
|
private long sentAtMs;
|
||||||
|
|
||||||
|
public String getTargetLogin() { return targetLogin; }
|
||||||
|
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
||||||
|
|
||||||
|
public int getAttemptedSessions() { return attemptedSessions; }
|
||||||
|
public void setAttemptedSessions(int attemptedSessions) { this.attemptedSessions = attemptedSessions; }
|
||||||
|
|
||||||
|
public int getSessionsWithPushConfig() { return sessionsWithPushConfig; }
|
||||||
|
public void setSessionsWithPushConfig(int sessionsWithPushConfig) { this.sessionsWithPushConfig = sessionsWithPushConfig; }
|
||||||
|
|
||||||
|
public int getDelivered() { return delivered; }
|
||||||
|
public void setDelivered(int delivered) { this.delivered = delivered; }
|
||||||
|
|
||||||
|
public int getFailed() { return failed; }
|
||||||
|
public void setFailed(int failed) { this.failed = failed; }
|
||||||
|
|
||||||
|
public long getSentAtMs() { return sentAtMs; }
|
||||||
|
public void setSentAtMs(long sentAtMs) { this.sentAtMs = sentAtMs; }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user