From 2537f323dc52739732cb8bd4aba3f37ecb2b1f86 Mon Sep 17 00:00:00 2001 From: Max Hauser Date: Tue, 10 Oct 2023 11:06:14 +0200 Subject: [PATCH] Add news also to the notification system (#2135) --- README.md | 1 + io-package.json | 149 ++++++++++++++++++++++++-- main.js | 160 ++++++++++++++++++++++++++-- src/src/dialogs/NewsAdminDialog.tsx | 1 + 4 files changed, 296 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 648410a56..4fe570daa 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ The icons may not be reused in other projects without the proper flaticon licens ## Changelog ### **WORK IN PROGRESS** * (foxriver76) JSON config component `port` does no longer mark port as occupied if it is only occupied on another host +* (foxriver76) register news as notifications * (foxriver76) if upgrade all is stopped on error, activate close button * (bluefox) Export/import objects with its values diff --git a/io-package.json b/io-package.json index 355902a5a..5a06dc445 100644 --- a/io-package.json +++ b/io-package.json @@ -182,15 +182,17 @@ "wwwDontUpload": true, "eraseOnUpload": true, "nogit": true, - "welcomeScreenPro": { - "link": "admin/index.html", - "name": "Admin", - "img": "admin/img/admin.png", - "color": "pink", - "order": 5, - "localLinks": "_default", - "localLink": true - }, + "welcomeScreenPro": [ + { + "link": "admin/index.html", + "name": "Admin", + "img": "admin/img/admin.png", + "color": "pink", + "order": 5, + "localLinks": "_default", + "localLink": true + } + ], "localLinks": { "_default": { "link": "%protocol%://%bind%:%port%", @@ -236,6 +238,135 @@ "loadingHideLogo": false, "loadingBackgroundImage": false }, + "notifications": [ + { + "scope": "news", + "name": { + "en": "News", + "de": "Nachrichten", + "ru": "Новости", + "pt": "Notícias", + "nl": "Nieuws", + "fr": "Actualités", + "it": "Notizie", + "es": "Noticias", + "pl": "News", + "uk": "Новини", + "zh-cn": "新闻" + }, + "description": { + "en": "These notifications represent news regarding installed adapters or general ioBroker information.", + "de": "Diese Benachrichtigungen enthalten Neuigkeiten zu installierten Adaptern oder allgemeine ioBroker-Informationen.", + "ru": "Эти уведомления представляют новости о установленных адаптерах или общей информации ioBroker.", + "pt": "Estas notificações representam notícias sobre adaptadores instalados ou informações gerais do ioBroker.", + "nl": "Deze berichten zijn nieuws over geïnstalleerde adapters of algemene ioBroker informatie.", + "fr": "Ces notifications représentent des nouvelles concernant les adaptateurs installés ou les informations générales ioBroker.", + "it": "Queste notifiche rappresentano notizie riguardanti adattatori installati o informazioni generali su ioBroker.", + "es": "Estas notificaciones representan noticias sobre adaptadores instalados o información general ioBroker.", + "pl": "Noty te reprezentują informacje dotyczące zainstalowanych adapterów lub ogólnie dostępnych informacji ioBrokera.", + "uk": "Ці повідомлення представляють новини про встановлені адаптери або загальні відомості про ioBroker.", + "zh-cn": "这些通知是有关安装的适应器或一般的气箱信息的新闻。." + }, + "categories": [ + { + "category": "info", + "name": { + "en": "General news", + "de": "Allgemeine Nachrichten", + "ru": "Общие новости", + "pt": "Notícia geral", + "nl": "Generaal", + "fr": "Nouvelles générales", + "it": "Notizie generali", + "es": "Noticias generales", + "pl": "Strona oficjalna", + "uk": "Новини", + "zh-cn": "新闻" + }, + "severity": "notify", + "description": { + "en": "These messages represent general news, which just have informal purpose and do not need to be read immediately.", + "de": "Diese Nachrichten stellen allgemeine Nachrichten dar, die nur informellen Zweck haben und nicht sofort gelesen werden müssen.", + "ru": "Эти сообщения представляют собой общие новости, которые просто имеют неформальную цель и не нужно читать немедленно.", + "pt": "Essas mensagens representam notícias gerais, que apenas têm um propósito informal e não precisam ser lidas imediatamente.", + "nl": "Deze berichten vertegenwoordigen algemene nieuws, wat informeel doel heeft en niet onmiddellijk hoeft te worden gelezen.", + "fr": "Ces messages représentent des nouvelles générales, qui ont juste un but informel et ne doivent pas être lus immédiatement.", + "it": "Questi messaggi rappresentano notizie generali, che hanno solo scopo informale e non devono essere letti immediatamente.", + "es": "Estos mensajes representan noticias generales, que sólo tienen un propósito informal y no necesitan ser leídos inmediatamente.", + "pl": "Wiadomości te reprezentują ogólnokrajowe wiadomości, które tylko mają nieformalny cel i nie muszą być odczytane natychmiast.", + "uk": "Ці повідомлення представляють загальні новини, які просто мають неформальне призначення і не потрібно негайно прочитати.", + "zh-cn": "这些信息是一般新闻,这只是非正式目的,不需要立即阅读。." + }, + "regex": [], + "limit": 10 + }, + { + "category": "warning", + "name": { + "en": "Important news", + "de": "Wichtige Nachrichten", + "ru": "Важные новости", + "pt": "Notícia importante", + "nl": "Belangrijk nieuws", + "fr": "Nouvelles importantes", + "it": "Notizie importanti", + "es": "Noticias importantes", + "pl": "Important news", + "uk": "Новини", + "zh-cn": "重要的新闻" + }, + "severity": "info", + "description": { + "en": "These messages represent adapter warnings and important changes in the near future.", + "de": "Diese Nachrichten stellen Adapterwarnungen und wichtige Veränderungen in der nahen Zukunft dar.", + "ru": "Эти сообщения представляют предупреждение о адаптере и важные изменения в ближайшем будущем.", + "pt": "Estas mensagens representam avisos de adaptadores e mudanças importantes no futuro próximo.", + "nl": "Deze berichten vertegenwoordigen adapter waarschuwingen en belangrijke veranderingen in de nabije toekomst.", + "fr": "Ces messages représentent des avertissements d'adaptateur et des changements importants dans un proche avenir.", + "it": "Questi messaggi rappresentano avvisi di adattatore e cambiamenti importanti nel prossimo futuro.", + "es": "Estos mensajes representan advertencias de adaptador y cambios importantes en el futuro cercano.", + "pl": "Wiadomości te reprezentują ostrzeżenia adaptatora i ważne zmiany w najbliższej przyszłości.", + "uk": "Ці повідомлення представляють попередження та важливі зміни в найближчому майбутньому.", + "zh-cn": "这些信息是适应的预警和近期的重要变化。." + }, + "regex": [], + "limit": 10 + }, + { + "category": "danger", + "name": { + "en": "Very important news", + "de": "Sehr wichtige Nachrichten", + "ru": "Очень важные новости", + "pt": "Notícia muito importante", + "nl": "Heel belangrijk", + "fr": "Nouvelles très importantes", + "it": "Notizie molto importanti", + "es": "Noticias muy importantes", + "pl": "Ważne wiadomości", + "uk": "Останні новини", + "zh-cn": "非常重要的新闻" + }, + "severity": "alert", + "description": { + "en": "These notifications are very important. They may give you a hint that an adapter upgrade is required right now to maintain functionality.", + "de": "Diese Benachrichtigungen sind sehr wichtig. Sie können Ihnen einen Hinweis geben, dass ein Adapter-Upgrade jetzt erforderlich ist, um die Funktionalität zu erhalten.", + "ru": "Эти уведомления очень важны. Они могут дать вам подсказку, что обновление адаптера требуется прямо сейчас для поддержания функциональности.", + "pt": "Estas notificações são muito importantes. Eles podem lhe dar uma dica de que uma atualização do adaptador é necessária agora para manter a funcionalidade.", + "nl": "Deze berichten zijn heel belangrijk. Ze kunnen je een hint geven dat een adapter upgrade nu nodig is om functionaliteit te behouden.", + "fr": "Ces notifications sont très importantes. Ils peuvent vous donner un indice qu'une mise à niveau d'adaptateur est nécessaire pour maintenir la fonctionnalité.", + "it": "Queste notifiche sono molto importanti. Essi possono dare un suggerimento che un aggiornamento adattatore è necessario in questo momento per mantenere la funzionalità.", + "es": "Estas notificaciones son muy importantes. Pueden darle una pista de que se requiere una actualización del adaptador ahora mismo para mantener la funcionalidad.", + "pl": "Te informacje są bardzo ważne. Mogą dać wskazówki, że ulepszanie adapteru jest niezbędne do utrzymania funkcji.", + "uk": "Ці повідомлення дуже важливі. Вони можуть надати вам підказку, що оновлення адаптера потрібно прямо зараз для підтримки функціональності.", + "zh-cn": "这些通知非常重要。 他们可以向你说明,适应人员升级现在需要保持功能。." + }, + "regex": [], + "limit": 10 + } + ] + } + ], "objects": [], "instanceObjects": [ { diff --git a/main.js b/main.js index a2e5b099b..411abbcfb 100644 --- a/main.js +++ b/main.js @@ -15,7 +15,8 @@ const semver = require('semver'); const axios = require('axios').default; -const fs = require('fs'); +const fs = require('node:fs'); +const os = require('node:os'); const utils = require('@iobroker/adapter-core'); // Get common adapter utils const getInstalledInfo = utils.commonTools.getInstalledInfo; @@ -679,10 +680,10 @@ class Admin extends utils.Adapter { return ( this.getStateAsync('info.newsETag') .then(state => { - oldEtag = state && state.val; + oldEtag = state?.val; return axios .get('https://iobroker.live/repo/news-hash.json', { - timeout: 13000, + timeout: 13_000, validateStatus: status => status < 400, }) .then(response => response.data) @@ -699,7 +700,7 @@ class Admin extends utils.Adapter { newEtag = etag.hash; return axios .get('https://iobroker.live/repo/news.json', { - timeout: 14000, + timeout: 14_000, validateStatus: status => status < 400, }) .then(response => response.data) @@ -722,7 +723,7 @@ class Admin extends utils.Adapter { .then(state => { try { /** @ts-expect-error */ - oldNews = state && state.val ? JSON.parse(state.val) : []; + oldNews = state?.val ? JSON.parse(state.val) : []; } catch (e) { oldNews = []; } @@ -734,7 +735,7 @@ class Admin extends utils.Adapter { .then(lastState => { // find time of last ID let time = ''; - if (lastState && lastState.val) { + if (lastState?.val) { const item = oldNews.find(item => item.id === lastState.val); if (item) { time = item.created; @@ -761,6 +762,7 @@ class Admin extends utils.Adapter { } if (originalOldNews !== JSON.stringify(oldNews)) { + this.registerNewsNotifications(oldNews, lastState?.val); return this.setStateAsync('info.newsFeed', JSON.stringify(oldNews), true); } else { return Promise.resolve(); @@ -775,6 +777,152 @@ class Admin extends utils.Adapter { ); } + /** + * Add the nes to the notification system + * + * @param {Record[]} messages sorted news + * @param {string | undefined | null} lastMessageId lastMessageId, all after this has already been seen + * + * @return {Promise} + */ + async registerNewsNotifications(messages, lastMessageId) { + const adapters = await this.getObjectViewAsync('system', 'adapter', { + startkey: 'system.adapter.\u0000', + endkey: 'system.adapter.\u9999', + }); + + const operatingSystem = os.platform(); + + const instances = await this.getObjectViewAsync('system', 'instance', { + startkey: 'system.adapter.\u0000', + endkey: 'system.adapter.\u9999', + }); + + const activeRepo = (await this.getForeignObjectAsync('system.config'))?.common.activeRepo; + const uuid = (await this.getForeignObjectAsync('system.meta.uuid'))?.native.uuid; + const nodeVersion = process.version; + const npmVersion = await this.getNpmVersion(); + + const today = Date.now(); + for (const message of messages) { + if (!message) { + continue; + } + + if (message.id === lastMessageId) { + break; + } + let showIt = true; + + if (showIt && message['date-start'] && new Date(message['date-start']).getTime() > today) { + showIt = false; + } else if (showIt && message['date-end'] && new Date(message['date-end']).getTime() < today) { + showIt = false; + } else if (showIt && message.conditions && Object.keys(message.conditions).length > 0) { + Object.keys(message.conditions).forEach(key => { + if (showIt) { + const adapter = adapters.rows.find(adapter => adapter.id === `system.adapter.${key}`); + const condition = message.conditions[key]; + + if (!adapter && condition !== '!installed') { + showIt = false; + } else if (adapter && condition === '!installed') { + showIt = false; + } else if (adapter && condition === 'active') { + showIt = this.checkActive(key, instances); + } else if (adapter && condition === '!active') { + showIt = !this.checkActive(key, instances); + } else if (adapter?.value) { + showIt = this.checkConditions(condition, adapter.value.common.version); + } + } + }); + } + + if (showIt && message['node-version']) { + showIt = this.checkConditions(message['node-version'], nodeVersion); + } + if (showIt && message['npm-version']) { + showIt = this.checkConditions(message['npm-version'], npmVersion); + } + if (showIt && message.os) { + showIt = operatingSystem === message.os; + } + if (showIt && message.repo) { + // If multi-repo + if (Array.isArray(activeRepo)) { + showIt = activeRepo.includes(message.repo); + } else { + showIt = activeRepo === message.repo; + } + } + if (showIt && message.uuid) { + if (Array.isArray(message.uuid)) { + showIt = uuid && message.uuid.find(msgUuid => uuid === msgUuid); + } else { + showIt = !!(uuid && uuid === message.uuid); + } + } + + if (showIt) { + this.log.info(`register notification ${message.class}`); + await this.registerNotification('news', message.class, message.title.en + '\n' + message.content.en); + } + } + } + + /** + * Check if adapter is active + * + * @param {string} adapterName + * @param {Awaited>} instances + * @return {boolean} + */ + checkActive(adapterName, instances) { + return !!Object.keys(instances) + .filter(id => id.startsWith(`adapter.system.${adapterName}.`)) + .find(id => instances.rows.find(row => id === row.id)?.value?.common.enabled); + } + + /** + * Check if conditions met + * + * @param {string} condition + * @param {string} installedVersion + * @return {boolean} + */ + checkConditions(condition, installedVersion) { + if (condition.startsWith('equals')) { + const vers = condition.substring(7, condition.length - 1).trim(); + return installedVersion === vers; + } + if (condition.startsWith('bigger') || condition.startsWith('greater')) { + const vers = condition.substring(7, condition.length - 1).trim(); + try { + return semver.gt(vers, installedVersion); + } catch (e) { + return false; + } + } else if (condition.startsWith('smaller')) { + const vers = condition.substring(8, condition.length - 1).trim(); + try { + return semver.lt(installedVersion, vers); + } catch (e) { + return false; + } + } else if (condition.startsWith('between')) { + const vers1 = condition.substring(8, condition.indexOf(',')).trim(); + const vers2 = condition.substring(condition.indexOf(',') + 1, condition.length - 1).trim(); + try { + return semver.gte(installedVersion, vers1) && semver.lte(installedVersion, vers2); + } catch (e) { + return false; + } + } else { + return true; + } + } + /** * Get current npm version from controller * @returns {Promise} diff --git a/src/src/dialogs/NewsAdminDialog.tsx b/src/src/dialogs/NewsAdminDialog.tsx index 74da9b0b5..bd620926f 100644 --- a/src/src/dialogs/NewsAdminDialog.tsx +++ b/src/src/dialogs/NewsAdminDialog.tsx @@ -189,6 +189,7 @@ export const checkMessages = (messages: Message[], lastMessageId: string, contex if (!message) { continue; } + if (message.id === lastMessageId) { break; }