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
combined.log
error.log
debug.log

View File

@@ -1,7 +1,7 @@
const config = require('./config');
const winston = require('winston');
const { getAllQueries } = require('./pihole');
const { filterQueries, addPoint, getPointsCount, checkAndExpirePoints, clearPoints } = require('./points');
const { addPoint, getPointsCount, checkAndExpirePoints, clearPoints } = require('./points');
const { triggerAutomation } = require('./homeAssistant');
const discord = require('./discord');
@@ -14,7 +14,7 @@ const logger = winston.createLogger({
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
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) {
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) {
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.');
}
// Validate Discord webhook URL if provided
if (config.notifications && config.notifications.discordWebhookUrl) {
try {
new URL(config.notifications.discordWebhookUrl);
@@ -62,17 +60,13 @@ async function main() {
checkAndExpirePoints(logger);
const queries = await getAllQueries(config, logger);
const queries = await getAllQueries(config, logger, config.filter.strings);
if (queries) {
console.log('DEBUG: Queries Data:', JSON.stringify(queries.data, null, 2));
console.log('DEBUG: Filter Strings:', JSON.stringify(config.filter.strings, null, 2));
const matchingDomains = filterQueries(queries, config.filter.strings);
if (matchingDomains.size > 0) {
logger.info(`Found ${matchingDomains.size} matching domains.`);
for (const domain of matchingDomains) {
addPoint(domain, config.pointExpirationSeconds, logger, config, discord);
if (queries && queries.data) {
if (queries.data.length > 0) {
logger.info(`Found ${queries.data.length} matching domains.`);
for (const query of queries.data) {
addPoint(query.domain, config.pointExpirationSeconds, logger, config, discord);
}
}
@@ -86,4 +80,4 @@ async function main() {
}, config.pollingIntervalSeconds * 1000);
}
main();
main();

View File

@@ -1,5 +1,14 @@
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) {
if (!webhookUrl) {
logger.warn('Discord webhook URL is not configured. Skipping message.');
@@ -14,4 +23,4 @@ async function sendMessage(webhookUrl, message, logger) {
}
}
module.exports = { sendMessage };
module.exports = { sendMessage };

View File

@@ -29,56 +29,54 @@ async function login(config, logger) {
}
}
async function getAllQueries(config, logger) {
async function getAllQueries(config, logger, filterStrings) {
const requiresAuth = !!config.pihole.password;
// If no password is provided, do not attempt to log in.
if (!requiresAuth) {
const piholeApiUrl = `${getApiBaseUrl(config.pihole.host)}/queries`;
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;
}
}
// Login if required and we don't have a token
if (!apiToken) {
if (requiresAuth && !apiToken) {
const loggedIn = await login(config, logger);
if (!loggedIn) {
return null; // Stop if login fails
}
}
const queriesUrl = `${getApiBaseUrl(config.pihole.host)}/queries`;
const options = {
headers: {},
};
if (requiresAuth) {
options.headers['Authorization'] = apiToken;
}
try {
const response = await axios.get(queriesUrl, options);
// The new API nests the data differently
return { data: response.data.queries };
} catch (error) {
if (requiresAuth && error.response && error.response.status === 401) {
logger.warn('Pi-hole session token expired or invalid. Attempting to log in again...');
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('Error fetching data from Pi-hole v6 API:', { message: error.message });
return null;
}
const allMatchingQueries = [];
const piholeApiUrl = new URL(`${getApiBaseUrl(config.pihole.host)}/queries`);
const now = Math.floor(Date.now() / 1000);
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 options = {
headers: {},
};
if (requiresAuth) {
options.headers['Authorization'] = apiToken;
}
try {
const response = await axios.get(piholeApiUrl.toString(), options);
if (response.data && response.data.queries) {
allMatchingQueries.push(...response.data.queries);
}
} catch (error) {
let errorMessage = `Error fetching queries for domain filter \"${filter}\".`;
if (requiresAuth && error.response && error.response.status === 401) {
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
}
logger.error(errorMessage, { message: error.message });
}
}
const uniqueQueries = Array.from(new Map(allMatchingQueries.map(q => [q.id, q])).values());
return { data: uniqueQueries };
}
module.exports = { getAllQueries };

View File

@@ -1,34 +1,23 @@
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) {
const now = Date.now();
const expiration = now + expirationSeconds * 1000;
const pointExists = points.has(domain);
points.set(domain, expiration);
logger.info(`Point added for ${domain}. Total points: ${points.size}`);
if (pointExists) {
logger.info(`Point expiration updated for ${domain}. Total points: ${points.size}`);
} else {
logger.info(`Point added for ${domain}. Total points: ${points.size}`);
}
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.');
}
module.exports = { filterQueries, addPoint, getPointsCount, checkAndExpirePoints, clearPoints };
module.exports = { addPoint, getPointsCount, checkAndExpirePoints, clearPoints };