diff --git a/lib/actionButton.js b/lib/actionButton.js new file mode 100644 index 0000000..cd9798c --- /dev/null +++ b/lib/actionButton.js @@ -0,0 +1,91 @@ +'use strict'; + +class ActionButton +{ + constructor(action, label, clear = false) + { + this.action = action; + this.label = label; + this.clear = clear; + } + + isValidUrl(urlString) + { + const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string + '(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator + return !!urlPattern.test(urlString); + } +} + +class ActionButtonView extends ActionButton +{ + constructor(label, url, clear = false) + { + super('view', label, clear); + + this.url = url; + } + + getData() + { + return { + action: this.action, + label: this.label, + url: this.url, + clear: this.clear + }; + } +} + +class ActionButtonHTTP extends ActionButton +{ + constructor(label, url, clear = false) + { + super('http', label, clear); + + this.url = url; + this.method = 'POST'; + this.headers = {}; + this.body = ''; + } + + getData() + { + return { + action: this.action, + label: this.label, + url: this.url, + method: this.method, + headers: this.headers, + boddy: this.body, + clear: this.clear + }; + } + + setMethod(method) + { + this.method = method; + } + + setHeaders(headers) + { + this.headers = headers; + } + + setBody(body) + { + this.body = body; + } +} + +class ActionButtonAB extends ActionButton +{ + +} + + +module.exports = { ActionButton, ActionButtonView, ActionButtonHTTP, ActionButtonAB }; \ No newline at end of file diff --git a/lib/message.js b/lib/message.js new file mode 100644 index 0000000..6307810 --- /dev/null +++ b/lib/message.js @@ -0,0 +1,64 @@ +'use strict'; + + +class Message { + constructor(topic, title, message, priority = 3) + { + this.topic = topic; + this.title = title; + this.message = message; + this.priority = priority; + + this.delay = 0; + this.tags = {}; + this.clickURL = null; + + this.attachment = null; + this.actionBtns = {}; + } + + addDelay() + { + this.delay = 5; + } + + addTag(tag) + { + this.tags[this.tags.length + 1] = tag; + } + + addClickURL(url) + { + this.clickURL = url; + } + + addAttachment(attachFromURL) + { + if (!this.isValidUrl(attachFromURL)) return; + + this.attachment = { + attach: attachFromURL, + filename: attachFromURL.substring(attachFromURL.lastIndexOf('/')+1) + }; + } + + addActionBtn(actionButton) + { + this.actionBtns[this.actionBtns.length + 1] = actionButton; + } + + + + isValidUrl(urlString) + { + const urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string + '(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator + return !!urlPattern.test(urlString); + } +} + +module.exports = { Message }; \ No newline at end of file diff --git a/lib/topic.js b/lib/topic.js new file mode 100644 index 0000000..49c6685 --- /dev/null +++ b/lib/topic.js @@ -0,0 +1,108 @@ +'use strict'; + +const axios = require('axios'); +const EventSource = require('eventsource'); + +class Topic { + constructor(adapter, name, subscribed, authType = 0, username = '', password = '', accessToken) + { + this.adapter = adapter; + + this.name = name; + this.authType = authType; + this.username = username; + this.password = password; + this.accessToken = accessToken; + + this.shouldSubcribe = subscribed; + this.subscribed = false; + + this.topicKey = 'subscribedTopics.' + this.name; + this.messageKey = 'subscribedTopics.' + this.name + '.lastMessage.'; + this.connected = false; + this.eventSource = null; + } + + async subscribe() + { + this.adapter.log.debug('subscribe called ' + this.name); + + if (this.shouldSubcribe) { + this.adapter.log.debug('shouldSub ' + this.name); + + if (this.eventSource !== null && this.eventSource.readyState !== 2) { + return; + } + + await this.createSubsrciptionObjects(); + + this.eventSource = new EventSource(this.adapter.config.serverURL + '/' + this.name + '/sse', this.getHTTPConfig()); + this.eventSource.onmessage = (e) => { + this.adapter.log.debug('Received new message from topic ' + this.name); + + const msgData = JSON.parse(e.data); + + this.adapter.setState(this.topicKey + '.lastMessageRaw', {ack: true, val: e.data}); + this.adapter.setState(this.messageKey + 'id', {ack: true, val: msgData.id}); + this.adapter.setState(this.messageKey + 'time', {ack: true, val: msgData.time}); + this.adapter.setState(this.messageKey + 'tags', {ack: true, val: msgData.tags}); + this.adapter.setState(this.messageKey + 'event', {ack: true, val: msgData.event}); + this.adapter.setState(this.messageKey + 'title', {ack: true, val: msgData.title}); + this.adapter.setState(this.messageKey + 'value', {ack: true, val: msgData.message}); + this.adapter.setState(this.messageKey + 'expires', {ack: true, val: msgData.expires}); + this.adapter.setState(this.messageKey + 'priority', {ack: true, val: msgData.priority}); + }; + this.eventSource.onopen = () => { + if (!this.subscribed) { + this.adapter.log.debug('Subscription to topic ' + this.name + ' established.'); + this.adapter.setState(this.topicKey + '.connected', {ack: true, val: true}); + this.subscribed = true; + } + }; + this.eventSource.onerror = (e) => { + this.adapter.log.error('Subscription to topic ' + this.name + ' cant be established or lost. Status code: ' + e.status + ' Error: ' + e.message); + this.adapter.setState(this.topicKey + '.connected', {ack: true, val: false}); + }; + } + } + + async createSubsrciptionObjects() + { + await this.adapter.setObjectNotExists(this.topicKey, { type: 'channel', common: { name: this.adapter.config.serverURL + '/' + this.name, role: 'subscription' } }); + await this.adapter.setObjectNotExists(this.topicKey + '.lastMessageRaw', { type: 'state', common: { name: 'Last message RAW', role: 'message', type: 'string' } }); + await this.adapter.setObjectNotExists(this.topicKey + '.connected', { type: 'state', common: { name: 'Subscribtion state', role: 'state', type: 'boolean' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'id', { type: 'state', common: { name: 'ID', role: '', type: 'string' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'tags', { type: 'state', common: { name: 'Tags', role: '', type: 'string' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'time', { type: 'state', common: { name: 'Time', role: '', type: 'number' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'event', { type: 'state', common: { name: 'Event type', role: '', type: 'string' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'title', { type: 'state', common: { name: 'Title', role: '', type: 'string' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'value', { type: 'state', common: { name: 'Content', role: '', type: 'string' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'expires', { type: 'state', common: { name: 'Expire', role: '', type: 'number' } }); + await this.adapter.setObjectNotExists(this.messageKey + 'priority', { type: 'state', common: { name: 'Priority', role: '', type: 'number' } }); + } + + async sendMessage(message) + { + const data = message.getData(); + axios.post(this.adapter.config.serverURL, data, this.getHTTPConfig()) + .catch(err => { + this.adapter.log.error(`Ntfy error: ${err}`); + this.adapter.log.error(`Ntfy error: ${JSON.stringify(err.response.data)}`); + this.adapter.log.error(`Ntfy with config: ${JSON.stringify(this.getHTTPConfig())}`); + }); + } + + getHTTPConfig() + { + switch(this.authType) { + case 1: + return { headers: { 'Authorization': 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64') } }; + case 2: + return { headers: { 'Authorization': 'Bearer ' + this.accessToken } }; + default: + return {}; + } + } +} + +module.exports = { Topic }; \ No newline at end of file diff --git a/main.js b/main.js index 7ceadbd..5e6bd79 100644 --- a/main.js +++ b/main.js @@ -2,9 +2,13 @@ const utils = require('@iobroker/adapter-core'); const axios = require('axios'); -const EventSource = require('eventsource'); -class ntfy extends utils.Adapter { + +const { Topic } = require('./lib/topic'); + +const { ActionButtonView } = require('./lib/message/actionButton/actionButtonView.js'); + +class ntfy extends utils.Adapter { /** * @param {Partial} [options={}] @@ -18,12 +22,15 @@ class ntfy extends utils.Adapter { this.messageTime = 0; this.messageText = ''; + this.topics = {}; this.topicSubscriptionData = {}; this.checkSubscriptionsTimeout = null; this.on('ready', this.onReady.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); + + let test = new ActionButtonView() } async onReady() @@ -37,9 +44,11 @@ class ntfy extends utils.Adapter { this.setState('info.connection', true, true); + this.topics = this.getTopics(); + await this.subscribeTopics(); - this.startCheckSubscriptions(); + //this.startCheckSubscriptions(); } onUnload(callback) @@ -68,16 +77,11 @@ class ntfy extends utils.Adapter { async onMessage(obj) { if (typeof obj === 'object' && obj.message) { - if (obj.message === 'checkSubscriptions') { - this.startCheckSubscriptions(); - return; - } if (obj.command === 'send') { if (await this.checkConfig()) { this.sendMessage(obj); } } - } if (typeof obj === 'object' && obj.command) { @@ -154,105 +158,18 @@ class ntfy extends utils.Adapter { axiosConfig = { headers: { 'Authorization': 'Bearer ' + this.config.defaultAccessToken } }; } - axios.post(this.config.serverURL, json, axiosConfig) - .catch(err => { - this.log.error(`Ntfy error: ${err}`); - this.log.error(`Ntfy error: ${JSON.stringify(err.response.data)}`); - this.log.error(`Ntfy with config: ${JSON.stringify(axiosConfig)}`); - }); - } - - async subscribeTopics() - { - if (this.config.defaultSubscribed) { - await this.subscribeTopic(this.config.defaultTopic, this.config.defaultTopicAuth, this.config.defaultUsername, this.config.defaultPassword, this.config.defaultAccessToken); - } - if (typeof this.config.presetTopics === 'object' || this.config.presetTopics.length > 0) { - this.config.presetTopics.forEach( async (presetTopic) => { - if (presetTopic.presetTopicSubscribed) { - await this.subscribeTopic(presetTopic.presetTopicName, presetTopic.presetTopicAuth, presetTopic.presetTopicUsername, presetTopic.presetTopicPassword, presetTopic.presetTopicAccessToken); - } - }); - } } - async subscribeTopic (topicName, authType, username = '', password = '', accessToken = '') + async subscribeTopics() { - this.topicSubscriptionData[topicName] = this.topicSubscriptionData[topicName] ?? { - topicData: { - authType: authType, - username: username, - password: password, - accessToken: accessToken - }, - topicKey: 'subscribedTopics.' + topicName, - messageKey: 'subscribedTopics.' + topicName + '.lastMessage.', - connected: false, - eventSource : null - }; - - if (this.topicSubscriptionData[topicName].eventSource !== null && this.topicSubscriptionData[topicName].eventSource.readyState !== 2) { - return; + for (const [index, topic] of Object.entries(this.topics)) { + await topic.subscribe(); } - - await this.createSubsrciptionObjects(topicName); - - let subscribtionEventSourceConfig = {}; - - switch(authType) { - case 1: - subscribtionEventSourceConfig = { headers: { 'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64') } }; - break; - case 2: - subscribtionEventSourceConfig = { headers: { 'Authorization': 'Bearer ' + accessToken } }; - break; - } - - this.topicSubscriptionData[topicName].eventSource = new EventSource(this.config.serverURL + '/' + topicName + '/sse', subscribtionEventSourceConfig); - this.topicSubscriptionData[topicName].eventSource.onmessage = (e) => { - this.log.debug('Received new message from topic ' + topicName); - - const msgData = JSON.parse(e.data); - - this.setState(this.topicSubscriptionData[topicName].topicKey + '.lastMessageRaw', {ack: true, val: e.data}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'id', {ack: true, val: msgData.id}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'time', {ack: true, val: msgData.time}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'tags', {ack: true, val: msgData.tags}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'event', {ack: true, val: msgData.event}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'title', {ack: true, val: msgData.title}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'value', {ack: true, val: msgData.message}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'expires', {ack: true, val: msgData.expires}); - this.setState(this.topicSubscriptionData[topicName].messageKey + 'priority', {ack: true, val: msgData.priority}); - }; - this.topicSubscriptionData[topicName].eventSource.onopen = () => { - if (!this.topicSubscriptionData[topicName].connected) { - this.log.debug('Subscription to topic ' + topicName + ' established.'); - this.setState(this.topicSubscriptionData[topicName].topicKey + '.connected', {ack: true, val: true}); - this.topicSubscriptionData[topicName].connected = true; - } - }; - this.topicSubscriptionData[topicName].eventSource.onerror = (e) => { - this.log.error('Subscription to topic ' + topicName + ' cant be established or lost. Status code: ' + e.status + ' Error: ' + e.message); - this.setState(this.topicSubscriptionData[topicName].topicKey + '.connected', {ack: true, val: false}); - }; } - async createSubsrciptionObjects(topicName) - { - await this.setObjectNotExists(this.topicSubscriptionData[topicName].topicKey, { type: "channel", common: { name: this.config.serverURL + '/' + topicName, role: "subscription" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].topicKey + '.lastMessageRaw', { type: "state", common: { name: "Last message RAW", role: "message", type: "string" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].topicKey + '.connected', { type: "state", common: { name: "Subscribtion state", role: "state", type: "boolean" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'id', { type: 'state', common: { name: 'ID', role: '', type: "string" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'tags', { type: 'state', common: { name: 'Tags', role: '', type: "string" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'time', { type: 'state', common: { name: 'Time', role: '', type: "number" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'event', { type: 'state', common: { name: 'Event type', role: '', type: "string" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'title', { type: 'state', common: { name: 'Title', role: '', type: "string" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'value', { type: 'state', common: { name: 'Content', role: '', type: "string" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'expires', { type: 'state', common: { name: 'Expire', role: '', type: "number" } }); - await this.setObjectNotExists(this.topicSubscriptionData[topicName].messageKey + 'priority', { type: 'state', common: { name: 'Priority', role: '', type: "number" } }); - } +/* async startCheckSubscriptions() { for (const [topicName, subscription] of Object.entries(this.topicSubscriptionData)) { @@ -268,6 +185,24 @@ class ntfy extends utils.Adapter { } this.checkSubscriptionsTimeout = setTimeout( () => { this.startCheckSubscriptions(); }, 30000 ); } +*/ + + getTopics() + { + const topics = {}; + + // Add default topic + topics[0] = new Topic(this, this.config.defaultTopic, this.config.defaultSubscribed, this.config.defaultTopicAuth, this.config.defaultUsername, this.config.defaultPassword, this.config.defaultAccessToken); + + // Add preset topics + if (typeof this.config.presetTopics === 'object' && this.config.presetTopics.length > 0) { + this.config.presetTopics.forEach( (presetTopic, index) => { + topics[index + 1] = new Topic(this, presetTopic.presetTopicName, presetTopic.presetTopicSubscribed, presetTopic.presetTopicAuth, presetTopic.presetTopicUsername, presetTopic.presetTopicPassword, presetTopic.presetTopicAccessToken); + }); + } + + return topics; + } }