Merge pull request #1645 from symonbaikov/feature/apprise-config-options

feat: add operator config options for Apprise notifications (closes #1576)
This commit is contained in:
Daniel Hiller
2026-09-17 02:00:24 +02:00
committed by GitHub
8 changed files with 98 additions and 2 deletions
+9
View File
@@ -42,6 +42,15 @@ SECRET_KEY=notsecretkey
# will be sent through this proxy if set. # will be sent through this proxy if set.
# OUTGOING_PROXY=http://proxy:3128 # OUTGOING_PROXY=http://proxy:3128
# Apprise notifications configuration.
# Set to false to prevent users from configuring and sending Apprise notifications.
# APPRISE_ENABLED=false
# Comma-separated allowlist of Apprise schemas users may use (e.g. slack,telegram).
# When set, only schemas in this list are accepted; APPRISE_BLOCKED_SCHEMAS is ignored.
# APPRISE_ALLOWED_SCHEMAS=
# Comma-separated blocklist of Apprise schemas. Replaces the built-in blocked list.
# APPRISE_BLOCKED_SCHEMAS=syslog,dbus,kde,qt,glib,gnome,macosx,windows
# Set to true to expose the Swagger specification at /swagger.json # Set to true to expose the Swagger specification at /swagger.json
# SWAGGER_EXPOSED=false # SWAGGER_EXPOSED=false
@@ -68,6 +68,12 @@ const Errors = {
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
APPRISE_DISABLED: {
appriseDisabled: 'Apprise notifications are disabled',
},
SCHEMA_NOT_ALLOWED: {
schemaNotAllowed: 'Notification service schema is not allowed',
},
LIMIT_REACHED: { LIMIT_REACHED: {
limitReached: 'Limit reached', limitReached: 'Limit reached',
}, },
@@ -95,6 +101,12 @@ module.exports = {
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
appriseDisabled: {
responseType: 'forbidden',
},
schemaNotAllowed: {
responseType: 'unprocessableEntity',
},
limitReached: { limitReached: {
responseType: 'conflict', responseType: 'conflict',
}, },
@@ -125,6 +137,8 @@ module.exports = {
actorUser: currentUser, actorUser: currentUser,
request: this.req, request: this.req,
}) })
.intercept('appriseDisabled', () => Errors.APPRISE_DISABLED)
.intercept('schemaNotAllowed', () => Errors.SCHEMA_NOT_ALLOWED)
.intercept('limitReached', () => Errors.LIMIT_REACHED); .intercept('limitReached', () => Errors.LIMIT_REACHED);
return { return {
@@ -68,6 +68,12 @@ const Errors = {
USER_NOT_FOUND: { USER_NOT_FOUND: {
userNotFound: 'User not found', userNotFound: 'User not found',
}, },
APPRISE_DISABLED: {
appriseDisabled: 'Apprise notifications are disabled',
},
SCHEMA_NOT_ALLOWED: {
schemaNotAllowed: 'Notification service schema is not allowed',
},
LIMIT_REACHED: { LIMIT_REACHED: {
limitReached: 'Limit reached', limitReached: 'Limit reached',
}, },
@@ -95,6 +101,12 @@ module.exports = {
userNotFound: { userNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
appriseDisabled: {
responseType: 'forbidden',
},
schemaNotAllowed: {
responseType: 'unprocessableEntity',
},
limitReached: { limitReached: {
responseType: 'conflict', responseType: 'conflict',
}, },
@@ -118,6 +130,8 @@ module.exports = {
actorUser: currentUser, actorUser: currentUser,
request: this.req, request: this.req,
}) })
.intercept('appriseDisabled', () => Errors.APPRISE_DISABLED)
.intercept('schemaNotAllowed', () => Errors.SCHEMA_NOT_ALLOWED)
.intercept('limitReached', () => Errors.LIMIT_REACHED); .intercept('limitReached', () => Errors.LIMIT_REACHED);
return { return {
@@ -23,11 +23,28 @@ module.exports = {
}, },
exits: { exits: {
appriseDisabled: {},
schemaNotAllowed: {},
limitReached: {}, limitReached: {},
}, },
async fn(inputs) { async fn(inputs) {
const { values } = inputs; const { values } = inputs;
const { appriseEnabled, appriseAllowedSchemas, appriseBlockedSchemas } = sails.config.custom;
if (!appriseEnabled) {
throw 'appriseDisabled';
}
const schema = values.url.split(':')[0];
if (appriseAllowedSchemas.length > 0 && !appriseAllowedSchemas.includes(schema)) {
throw 'schemaNotAllowed';
}
if (appriseAllowedSchemas.length === 0 && appriseBlockedSchemas.includes(schema)) {
throw 'schemaNotAllowed';
}
const notificationServicesTotal = await sails.helpers.boards.getNotificationServicesTotal( const notificationServicesTotal = await sails.helpers.boards.getNotificationServicesTotal(
values.board.id, values.board.id,
@@ -19,11 +19,28 @@ module.exports = {
}, },
exits: { exits: {
appriseDisabled: {},
schemaNotAllowed: {},
limitReached: {}, limitReached: {},
}, },
async fn(inputs) { async fn(inputs) {
const { values } = inputs; const { values } = inputs;
const { appriseEnabled, appriseAllowedSchemas, appriseBlockedSchemas } = sails.config.custom;
if (!appriseEnabled) {
throw 'appriseDisabled';
}
const schema = values.url.split(':')[0];
if (appriseAllowedSchemas.length > 0 && !appriseAllowedSchemas.includes(schema)) {
throw 'schemaNotAllowed';
}
if (appriseAllowedSchemas.length === 0 && appriseBlockedSchemas.includes(schema)) {
throw 'schemaNotAllowed';
}
const notificationServicesTotal = await sails.helpers.users.getNotificationServicesTotal( const notificationServicesTotal = await sails.helpers.users.getNotificationServicesTotal(
values.user.id, values.user.id,
@@ -31,6 +31,17 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
const { appriseEnabled, appriseAllowedSchemas, appriseBlockedSchemas } = sails.config.custom;
if (!appriseEnabled) {
return;
}
const schemaConfig = JSON.stringify({
allowedSchemas: appriseAllowedSchemas,
blockedSchemas: appriseBlockedSchemas,
});
try { try {
await promisifyExecFile( await promisifyExecFile(
PYTHON_PATH, PYTHON_PATH,
@@ -39,6 +50,7 @@ module.exports = {
JSON.stringify(inputs.services), JSON.stringify(inputs.services),
inputs.title, inputs.title,
JSON.stringify(inputs.bodyByFormat), JSON.stringify(inputs.bodyByFormat),
schemaConfig,
], ],
{ {
env: { env: {
+4
View File
@@ -101,6 +101,10 @@ module.exports.custom = {
smtpPassword: process.env.SMTP_PASSWORD, smtpPassword: process.env.SMTP_PASSWORD,
smtpFrom: process.env.SMTP_FROM, smtpFrom: process.env.SMTP_FROM,
appriseEnabled: process.env.APPRISE_ENABLED !== 'false',
appriseAllowedSchemas: envToArray(process.env.APPRISE_ALLOWED_SCHEMAS),
appriseBlockedSchemas: envToArray(process.env.APPRISE_BLOCKED_SCHEMAS),
gravatarBaseUrl: process.env.GRAVATAR_BASE_URL, gravatarBaseUrl: process.env.GRAVATAR_BASE_URL,
/* Internal */ /* Internal */
+11 -2
View File
@@ -7,7 +7,7 @@ import logging
import apprise import apprise
BLOCKED_SCHEMAS_SET = { DEFAULT_BLOCKED_SCHEMAS_SET = {
'syslog', 'syslog',
'dbus', 'dbus',
'kde', 'kde',
@@ -45,13 +45,22 @@ if __name__ == '__main__':
services = json.loads(sys.argv[1]) services = json.loads(sys.argv[1])
title = sys.argv[2] title = sys.argv[2]
body_by_format = json.loads(sys.argv[3]) body_by_format = json.loads(sys.argv[3])
schema_config = json.loads(sys.argv[4]) if len(sys.argv) > 4 else {}
allowed_schemas = set(schema_config.get('allowedSchemas', []))
custom_blocked = schema_config.get('blockedSchemas', [])
blocked_schemas = set(custom_blocked) if custom_blocked else DEFAULT_BLOCKED_SCHEMAS_SET
errors = [] errors = []
for service in services: for service in services:
url = service['url'] url = service['url']
schema = url.split(':')[0] schema = url.split(':')[0]
if schema in BLOCKED_SCHEMAS_SET: if allowed_schemas and schema not in allowed_schemas:
errors.append(f'[{schema}] Schema not in allowed list')
continue
if schema in blocked_schemas:
errors.append(f'[{schema}] Blocked service schema') errors.append(f'[{schema}] Blocked service schema')
continue continue