Fully Functioning
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ SPEC.md
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
combined.log
|
combined.log
|
||||||
error.log
|
error.log
|
||||||
|
debug.log
|
||||||
|
|||||||
22
src/app.js
22
src/app.js
@@ -1,7 +1,7 @@
|
|||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const { getAllQueries } = require('./pihole');
|
const { getAllQueries } = require('./pihole');
|
||||||
const { filterQueries, addPoint, getPointsCount, checkAndExpirePoints, clearPoints } = require('./points');
|
const { addPoint, getPointsCount, checkAndExpirePoints, clearPoints } = require('./points');
|
||||||
const { triggerAutomation } = require('./homeAssistant');
|
const { triggerAutomation } = require('./homeAssistant');
|
||||||
const discord = require('./discord');
|
const discord = require('./discord');
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ const logger = winston.createLogger({
|
|||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console({ format: winston.format.simple() }),
|
new winston.transports.Console({ format: winston.format.simple() }),
|
||||||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||||
new winston.transports.File({ filename: 'combined.log' }),
|
new winston.transports.File({ filename: 'combined.log' })
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ function validateConfig(config) {
|
|||||||
if (!config.pihole || !config.pihole.host) {
|
if (!config.pihole || !config.pihole.host) {
|
||||||
throw new Error('Pi-hole configuration is missing host.');
|
throw new Error('Pi-hole configuration is missing host.');
|
||||||
}
|
}
|
||||||
// Password is now optional, so we don't validate it here.
|
|
||||||
|
|
||||||
if (!config.homeAssistant || !config.homeAssistant.host || !config.homeAssistant.apiToken || !config.homeAssistant.automation) {
|
if (!config.homeAssistant || !config.homeAssistant.host || !config.homeAssistant.apiToken || !config.homeAssistant.automation) {
|
||||||
throw new Error('Home Assistant configuration is missing or incomplete.');
|
throw new Error('Home Assistant configuration is missing or incomplete.');
|
||||||
@@ -35,7 +34,6 @@ function validateConfig(config) {
|
|||||||
logger.warn('pointExpirationSeconds not set in config, defaulting to 300.');
|
logger.warn('pointExpirationSeconds not set in config, defaulting to 300.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Discord webhook URL if provided
|
|
||||||
if (config.notifications && config.notifications.discordWebhookUrl) {
|
if (config.notifications && config.notifications.discordWebhookUrl) {
|
||||||
try {
|
try {
|
||||||
new URL(config.notifications.discordWebhookUrl);
|
new URL(config.notifications.discordWebhookUrl);
|
||||||
@@ -62,17 +60,13 @@ async function main() {
|
|||||||
|
|
||||||
checkAndExpirePoints(logger);
|
checkAndExpirePoints(logger);
|
||||||
|
|
||||||
const queries = await getAllQueries(config, logger);
|
const queries = await getAllQueries(config, logger, config.filter.strings);
|
||||||
|
|
||||||
if (queries) {
|
if (queries && queries.data) {
|
||||||
console.log('DEBUG: Queries Data:', JSON.stringify(queries.data, null, 2));
|
if (queries.data.length > 0) {
|
||||||
console.log('DEBUG: Filter Strings:', JSON.stringify(config.filter.strings, null, 2));
|
logger.info(`Found ${queries.data.length} matching domains.`);
|
||||||
const matchingDomains = filterQueries(queries, config.filter.strings);
|
for (const query of queries.data) {
|
||||||
|
addPoint(query.domain, config.pointExpirationSeconds, logger, config, discord);
|
||||||
if (matchingDomains.size > 0) {
|
|
||||||
logger.info(`Found ${matchingDomains.size} matching domains.`);
|
|
||||||
for (const domain of matchingDomains) {
|
|
||||||
addPoint(domain, config.pointExpirationSeconds, logger, config, discord);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to a Discord webhook.
|
||||||
|
* @param {string} webhookUrl The Discord webhook URL.
|
||||||
|
* @param {string} message The message to send.
|
||||||
|
* @param {object} logger The Winston logger instance.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @see https://discord.com/developers/docs/resources/webhook#execute-webhook
|
||||||
|
* @warning This function does not handle Discord's rate limiting. If you send too many messages in a short period, you may be rate limited (HTTP 429).
|
||||||
|
*/
|
||||||
async function sendMessage(webhookUrl, message, logger) {
|
async function sendMessage(webhookUrl, message, logger) {
|
||||||
if (!webhookUrl) {
|
if (!webhookUrl) {
|
||||||
logger.warn('Discord webhook URL is not configured. Skipping message.');
|
logger.warn('Discord webhook URL is not configured. Skipping message.');
|
||||||
|
|||||||
@@ -29,30 +29,27 @@ async function login(config, logger) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllQueries(config, logger) {
|
async function getAllQueries(config, logger, filterStrings) {
|
||||||
const requiresAuth = !!config.pihole.password;
|
const requiresAuth = !!config.pihole.password;
|
||||||
|
|
||||||
// If no password is provided, do not attempt to log in.
|
if (requiresAuth && !apiToken) {
|
||||||
if (!requiresAuth) {
|
const loggedIn = await login(config, logger);
|
||||||
const piholeApiUrl = `${getApiBaseUrl(config.pihole.host)}/queries`;
|
if (!loggedIn) {
|
||||||
try {
|
|
||||||
const response = await axios.get(piholeApiUrl);
|
|
||||||
return { data: response.data.queries };
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error fetching data from password-less Pi-hole:', { message: error.message });
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login if required and we don't have a token
|
const allMatchingQueries = [];
|
||||||
if (!apiToken) {
|
const piholeApiUrl = new URL(`${getApiBaseUrl(config.pihole.host)}/queries`);
|
||||||
const loggedIn = await login(config, logger);
|
|
||||||
if (!loggedIn) {
|
const now = Math.floor(Date.now() / 1000);
|
||||||
return null; // Stop if login fails
|
const from = now - (config.pollingIntervalSeconds || 10);
|
||||||
}
|
piholeApiUrl.searchParams.set('from', from);
|
||||||
}
|
piholeApiUrl.searchParams.set('until', now);
|
||||||
|
|
||||||
|
for (const filter of filterStrings) {
|
||||||
|
piholeApiUrl.searchParams.set('domain', `*${filter}*`);
|
||||||
|
|
||||||
const queriesUrl = `${getApiBaseUrl(config.pihole.host)}/queries`;
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {},
|
headers: {},
|
||||||
};
|
};
|
||||||
@@ -62,23 +59,24 @@ async function getAllQueries(config, logger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(queriesUrl, options);
|
const response = await axios.get(piholeApiUrl.toString(), options);
|
||||||
// The new API nests the data differently
|
if (response.data && response.data.queries) {
|
||||||
return { data: response.data.queries };
|
allMatchingQueries.push(...response.data.queries);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let errorMessage = `Error fetching queries for domain filter \"${filter}\".`;
|
||||||
if (requiresAuth && error.response && error.response.status === 401) {
|
if (requiresAuth && error.response && error.response.status === 401) {
|
||||||
logger.warn('Pi-hole session token expired or invalid. Attempting to log in again...');
|
errorMessage = `Pi-hole session token expired or invalid while fetching queries for \"${filter}\". A new token will be requested on the next polling cycle.`;
|
||||||
apiToken = null; // Clear expired token
|
apiToken = null; // Clear expired token
|
||||||
const loggedIn = await login(config, logger);
|
|
||||||
if (loggedIn) {
|
|
||||||
logger.info('Retrying to get all queries with new session...');
|
|
||||||
return await getAllQueries(config, logger); // Retry the request once
|
|
||||||
}
|
}
|
||||||
return null;
|
logger.error(errorMessage, { message: error.message });
|
||||||
}
|
|
||||||
logger.error('Error fetching data from Pi-hole v6 API:', { message: error.message });
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uniqueQueries = Array.from(new Map(allMatchingQueries.map(q => [q.id, q])).values());
|
||||||
|
|
||||||
|
return { data: uniqueQueries };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = { getAllQueries };
|
module.exports = { getAllQueries };
|
||||||
@@ -1,34 +1,23 @@
|
|||||||
const points = new Map();
|
const points = new Map();
|
||||||
|
|
||||||
function filterQueries(queries, filterStrings) {
|
|
||||||
const matchingDomains = new Set();
|
|
||||||
if (!queries || !queries.data) {
|
|
||||||
return matchingDomains;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const query of queries.data) {
|
|
||||||
const { domain, status } = query;
|
|
||||||
// Status 1: Blocked by gravity
|
|
||||||
// Status 4: Blocked by regex/wildcard
|
|
||||||
if (status === '1' || status === '4') {
|
|
||||||
for (const filterString of filterStrings) {
|
|
||||||
if (domain.toLowerCase().includes(filterString.toLowerCase())) {
|
|
||||||
matchingDomains.add(domain);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchingDomains;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPoint(domain, expirationSeconds, logger, config, discord) {
|
function addPoint(domain, expirationSeconds, logger, config, discord) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expiration = now + expirationSeconds * 1000;
|
const expiration = now + expirationSeconds * 1000;
|
||||||
|
|
||||||
|
const pointExists = points.has(domain);
|
||||||
points.set(domain, expiration);
|
points.set(domain, expiration);
|
||||||
|
|
||||||
|
if (pointExists) {
|
||||||
|
logger.info(`Point expiration updated for ${domain}. Total points: ${points.size}`);
|
||||||
|
} else {
|
||||||
logger.info(`Point added for ${domain}. Total points: ${points.size}`);
|
logger.info(`Point added for ${domain}. Total points: ${points.size}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.notifications && config.notifications.discordWebhookUrl) {
|
if (config.notifications && config.notifications.discordWebhookUrl) {
|
||||||
discord.sendMessage(config.notifications.discordWebhookUrl, `Point added for ${domain}. Total points: ${points.size}`, logger);
|
const message = pointExists
|
||||||
|
? `Point expiration updated for ${domain}. Total points: ${points.size}`
|
||||||
|
: `Point added for ${domain}. Total points: ${points.size}`;
|
||||||
|
discord.sendMessage(config.notifications.discordWebhookUrl, message, logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,4 +40,4 @@ function clearPoints(logger) {
|
|||||||
logger.info('All points have been cleared.');
|
logger.info('All points have been cleared.');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { filterQueries, addPoint, getPointsCount, checkAndExpirePoints, clearPoints };
|
module.exports = { addPoint, getPointsCount, checkAndExpirePoints, clearPoints };
|
||||||
Reference in New Issue
Block a user