Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
- Slack
- Twitch
- Support for notifications
- Apprise
- Discord
- Email (SMTP)
- Home Assistant
- Pushover
- Slack
Expand Down
7 changes: 6 additions & 1 deletion app/constant/notifications.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"Apprise": {
"id": "Apprise",
"name": "Apprise",
"icon": "apprise.svg"
},
"Discord": {
"id": "Discord",
"name": "Discord",
Expand Down Expand Up @@ -34,4 +39,4 @@
"name": "Webhook",
"icon": "webhook.svg"
}
}
}
35 changes: 35 additions & 0 deletions app/constant/notifications/layout/apprise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { NotificationInputLayoutType } from '../../../types/constant/notifications';

export const AppriseLayout: NotificationInputLayoutType[] = [
{
type: 'group',
items: [
{
type: 'input',
isDataField: false,
key: 'friendlyName',
title: 'notification.input.friendly_name',
placeholder: 'Lunalytics',
},
{
type: 'password',
isDataField: false,
key: 'token',
title: 'Apprise Webhook URL',
placeholder: 'https://apprise.lunalytics.xyz/notify',
},
{
type: 'input',
isDataField: true,
key: 'urls',
title: 'Notification URL(s)',
placeholder:
'discord://webhook_id/webhook_token, pushover://user_key/app_token',
subtitle: {
text: 'Seperate multiple URLs with commas to send to multiple services. For more information, see the Apprise documentation: ',
link: 'https://github.com/caronc/apprise-api',
},
},
],
},
];
1 change: 1 addition & 0 deletions app/constant/notifications/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { AppriseLayout as Apprise } from './apprise';
export { DiscordLayout as Discord } from './discord';
export { EmailLayout as Email } from './email';
export { HomeAssistantLayout as HomeAssistant } from './homeAssistant';
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lunalytics",
"version": "0.10.9",
"version": "0.10.10",
"description": "Open source Node.js server/website monitoring tool",
"private": true,
"author": "KSJaay <ksjaay@gmail.com>",
Expand Down
1 change: 1 addition & 0 deletions public/notifications/apprise.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions scripts/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// import node_modules
import fs from 'fs';
import path from 'path';

// import local files
import logger from '../server/utils/logger.js';

const [, , fileName] = process.argv;

if (!fileName) {
logger.error('Please provide a file name as an argument.');
process.exit(1);
}

const appriseDirs = [
{
directory: path.resolve(process.cwd(), 'shared', 'notifications'),
extension: '.js',
},
{
directory: path.resolve(process.cwd(), 'server', 'notifications'),
extension: '.js',
},
{
directory: path.resolve(
process.cwd(),
'app',
'constant',
'notifications',
'layout'
),
extension: '.ts',
},
{
directory: path.resolve(
process.cwd(),
'shared',
'validators',
'notifications'
),
extension: '.js',
},
];

appriseDirs.forEach(({ directory, extension }) => {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
const filePath = path.join(directory, fileName.toLowerCase() + extension);
try {
fs.writeFileSync(filePath, '', { flag: 'wx' });
logger.info(`File created: ${filePath}`);
} catch (err) {
if (err.code === 'EEXIST') {
logger.warn(`File already exists: ${filePath}`);
} else {
logger.error(`Error creating file in ${filePath}: ${err.message}`);
}
}
});

const notificationJson = path.resolve(
process.cwd(),
'app',
'constant',
'notifications.json'
);

const data = fs.readFileSync(notificationJson, 'utf-8');
const notifications = JSON.parse(data);

const notificationName =
fileName.charAt(0).toUpperCase() + fileName.slice(1).toLowerCase();

if (!notifications[notificationName]) {
notifications[notificationName] = {
id: notificationName,
name: notificationName,
icon: `${notificationName.toLowerCase()}.svg`,
};

fs.writeFileSync(
notificationJson,
JSON.stringify(
Object.keys(notifications)
.sort()
.reduce((obj, key) => {
obj[key] = notifications[key];
return obj;
}, {}),
null,
2
),
'utf-8'
);
logger.info(
`Notification entry added for ${notificationName} in notifications.json. Please add ${notificationName.toLowerCase()}.svg`
);
}
56 changes: 56 additions & 0 deletions server/notifications/apprise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import axios from 'axios';
import NotificationBase from './base.js';
import NotificationReplacers from '../../shared/notifications/replacers/notification.js';
import { AppriseTemplateMessages } from '../../shared/notifications/apprise.js';

class Apprise extends NotificationBase {
name = 'Apprise';

async send(notification, monitor, heartbeat) {
try {
const template =
AppriseTemplateMessages[notification.messageType] ||
notification.payload;

const content = NotificationReplacers(template, monitor, heartbeat);

await axios.post(notification.token, {
...content,
urls: notification.data.urls.split(',').map((url) => url.trim()),
});
return this.success;
} catch (error) {
this.handleError(error);
}
}

async test(notification) {
try {
await axios.post(notification.token, {
title: 'This is a test message',
urls: notification.data.urls.split(',').map((url) => url.trim()),
});
return this.success;
} catch (error) {
this.handleError(error);
}
}

async sendRecovery(notification, monitor, heartbeat) {
try {
const template = AppriseTemplateMessages.recovery;

const content = NotificationReplacers(template, monitor, heartbeat);

await axios.post(notification.token, {
...content,
urls: notification.data.urls.split(',').map((url) => url.trim()),
});
return this.success;
} catch (error) {
this.handleError(error);
}
}
}

export default Apprise;
2 changes: 2 additions & 0 deletions server/notifications/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Apprise from './apprise.js';
import Discord from './discord.js';
import Email from './email.js';
import HomeAssistant from './homeAssistant.js';
Expand All @@ -7,6 +8,7 @@ import Slack from './slack.js';
import Webhook from './webhook.js';

const NotificationServices = {
Apprise,
Discord,
Email,
HomeAssistant,
Expand Down
20 changes: 20 additions & 0 deletions shared/notifications/apprise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const AppriseSchema = {};

const AppriseTemplateMessages = {
basic: {
title: 'Triggered: Service {{service_name}} is currently down!',
},
pretty: {
title: 'Triggered: Service {{service_name}} is currently down!',
body: '**Service Name**\n{{service_name}}\n\n**Service Address**\n{{service_address}}\n\n**Latency**\n{{heartbeat_latency}} ms\n\n**Error**\n{{heartbeat_message}}',
},
nerdy: {
title: 'Triggered: Service {{service_name}} is currently down!',
body: '**Service**\n```{{service_parsed_json}}```\n\n**Heartbeat**\n```{{heartbeat_parsed_json}}```',
},
recovery: {
title: 'Service {{service_name}} is back up!',
},
};

export { AppriseSchema, AppriseTemplateMessages };
2 changes: 2 additions & 0 deletions shared/notifications/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AppriseTemplateMessages } from './apprise';
import { DiscordTemplateMessages } from './discord';
import { EmailTemplateMessages } from './email';
import { HomeAssistantTemplateMessages } from './homeAssistant';
Expand All @@ -7,6 +8,7 @@ import { TelegramTemplateMessages } from './telegram';
import { WebhookTemplateMessages } from './webhook';

const NotificationsTemplates = {
Apprise: AppriseTemplateMessages,
Discord: DiscordTemplateMessages,
Email: EmailTemplateMessages,
HomeAssistant: HomeAssistantTemplateMessages,
Expand Down
40 changes: 40 additions & 0 deletions shared/validators/notifications/apprise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NotificationValidatorError } from '../../utils/errors.js';

const friendlyNameRegex = /^[a-zA-Z0-9_-]+$/;
const messageTypes = ['basic', 'pretty', 'nerdy'];

const Apprise = ({ messageType, friendlyName, token, urls }) => {
if (friendlyNameRegex && !friendlyNameRegex.test(friendlyName)) {
throw new NotificationValidatorError(
'friendlyName',
'Invalid Friendly Name. Must be alphanumeric, dashes, and underscores only.'
);
}

if (!messageTypes.includes(messageType)) {
throw new NotificationValidatorError('messageType', 'Invalid Message Type');
}

if (!token) {
throw new NotificationValidatorError(
'token',
'Invalid Apprise Webhook URL'
);
}

if (!urls) {
throw new NotificationValidatorError('urls', 'Invalid Apprise URLs');
}

return {
platform: 'Apprise',
messageType,
token,
friendlyName,
data: {
urls,
},
};
};

export default Apprise;
2 changes: 2 additions & 0 deletions shared/validators/notifications/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Apprise from './apprise.js';
import Discord from './discord.js';
import Email from './email.js';
import HomeAssistant from './homeAssistant.js';
Expand All @@ -7,6 +8,7 @@ import Telegram from './telegram.js';
import Webhook from './webhook.js';

const NotificationValidators = {
Apprise,
Discord,
Email,
HomeAssistant,
Expand Down