Fully Functioning

This commit is contained in:
2025-08-14 15:40:52 -05:00
parent 72a2819aea
commit 368bb3cd5b
5 changed files with 75 additions and 84 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ SPEC.md
package-lock.json package-lock.json
combined.log combined.log
error.log error.log
debug.log

View File

@@ -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);
} }
} }

View File

@@ -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.');

View File

@@ -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 };

View File

@@ -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 };