Fully Functioning
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ SPEC.md
|
||||
package-lock.json
|
||||
combined.log
|
||||
error.log
|
||||
debug.log
|
||||
|
||||
22
src/app.js
22
src/app.js
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 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 });
|
||||
if (requiresAuth && !apiToken) {
|
||||
const loggedIn = await login(config, logger);
|
||||
if (!loggedIn) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Login if required and we don't have a token
|
||||
if (!apiToken) {
|
||||
const loggedIn = await login(config, logger);
|
||||
if (!loggedIn) {
|
||||
return null; // Stop if login fails
|
||||
}
|
||||
}
|
||||
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 queriesUrl = `${getApiBaseUrl(config.pihole.host)}/queries`;
|
||||
const options = {
|
||||
headers: {},
|
||||
};
|
||||
@@ -62,23 +59,24 @@ async function getAllQueries(config, logger) {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(queriesUrl, options);
|
||||
// The new API nests the data differently
|
||||
return { data: response.data.queries };
|
||||
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) {
|
||||
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
|
||||
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;
|
||||
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 };
|
||||
@@ -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);
|
||||
|
||||
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 };
|
||||
Reference in New Issue
Block a user