Skip to content

refactor: modernize hooks system with async/await and native fetch #529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 105 additions & 64 deletions lib/hooks.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,129 @@
var Q = require('q');
var _ = require('lodash');
var request = require('request');

var logger = require("./utils/logger")("hooks");

var BOXID = null;
var HOOKS = {};
var POSTHOOKS = {
'users.auth': function(data) {
// Valid data
if (!_.has(data, "id") || !_.has(data, "name")
|| !_.has(data, "token") || !_.has(data, "email")) {
throw "Invalid authentication data";
}
const _ = require("lodash");
const logger = require("./utils/logger")("hooks");

let BOXID = null;
let HOOKS = {};
let SECRET_TOKEN = null;

const DEFAULT_TIMEOUT = 5000; // Default timeout for HTTP requests (in milliseconds)

// Post-processing hooks for specific events
const POSTHOOKS = {
"users.auth": function (data) {
if (
!_.has(data, "id") ||
!_.has(data, "name") ||
!_.has(data, "token") ||
!_.isString(data.email)
) {
throw new Error("Invalid authentication data");
}
return data;
}
},
};
var SECRET_TOKEN = null;

// Call hook
var use = function(hook, data) {
logger.log("call hook ", hook);
/**
* Call a hook.
* @param {string} hook - The name of the hook to call.
* @param {Object} data - The data to pass to the hook.
* @returns {Promise<any>} - Resolves with the result of the hook.
* @throws {Error} - Throws an error if the hook does not exist or fails.
*/
const use = async (hook, data) => {
logger.log(`Calling hook: '${hook}'`);

if (!HOOKS[hook]) return Q.reject("Hook '"+hook+"' doesn't exists");
if (!HOOKS[hook]) {
throw new Error(`Hook '${hook}' does not exist`);
}

return Q()
.then(function() {
var handler = HOOKS[hook];
let result;
const handler = HOOKS[hook];

if (_.isFunction(handler)) {
return Q(handler(data));
} else if (_.isString(handler)) {
var d = Q.defer();
if (_.isFunction(handler)) {
// Local function hook
result = await handler(data);
} else if (_.isString(handler)) {
// Remote HTTP hook
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);

// Do http requests
request.post(handler, {
'body': {
'id': BOXID,
'data': data,
'hook': hook
},
'headers': {
'Authorization': SECRET_TOKEN
try {
const response = await fetch(handler, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: SECRET_TOKEN,
},
'json': true,
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
d.resolve(body);
} else {
d.reject(new Error("Error with "+hook+" webhook: "+(body ? (body.error || body) : error.message)));
}
body: JSON.stringify({
id: BOXID,
data,
hook,
}),
signal: controller.signal,
});

return d.promise;
} else {
throw "Not a valid hook";
clearTimeout(timeout); // Clear the timeout after the request completes

if (!response.ok) {
const errBody = await response.text();
throw new Error(`Error with ${hook} webhook: ${errBody}`);
}

result = await response.json();
} catch (error) {
clearTimeout(timeout); // Ensure timeout is cleared on error
logger.error(`Error calling webhook '${hook}':`, error.message);
throw new Error(`Error with ${hook} webhook: ${error.message}`);
}
})
.then(function(data) {
if (POSTHOOKS[hook]) {
return POSTHOOKS[hook](data);
} else {
throw new Error(`Invalid hook type for '${hook}'`);
}

// Post-processing hook
if (POSTHOOKS[hook]) {
try {
result = POSTHOOKS[hook](result);
} catch (error) {
logger.error(
`Error in post-processing for hook '${hook}':`,
error.message
);
throw new Error(
`Post-processing error for '${hook}': ${error.message}`
);
}
return data;
})
.fail(function(err) {
logger.error("Error with hook:");
logger.exception(err, false);
}

return Q.reject(err);
});
return result;
};

// Init hook system
var init = function(options) {
logger.log("init hooks");
/**
* Initialize the hook system.
* @param {Object} options - Configuration options.
* @param {string} options.id - The unique ID for the system.
* @param {Object} options.hooks - An object defining all hooks.
* @param {string} options.secret - The secret token for authorization.
* @throws {Error} - Throws an error if the options are invalid.
*/
const init = (options) => {
logger.log("Initializing hooks");

if (!_.isObject(options) || !_.isObject(options.hooks)) {
throw new Error('Invalid options: "hooks" must be an object');
}

if (!_.isString(options.id) || !_.isString(options.secret)) {
throw new Error('Invalid options: "id" and "secret" must be strings');
}

BOXID = options.id;
HOOKS = options.hooks;
SECRET_TOKEN = options.secret;

logger.log(`Hooks initialized with ID: '${BOXID}'`);
};

module.exports = {
init: init,
use: use
init,
use,
};

87 changes: 56 additions & 31 deletions lib/socket.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,91 @@
var Q = require('q');
var _ = require('lodash');
var sockjs = require('sockjs');
var events = require('events');
const Q = require("q");
const _ = require("lodash");
const sockjs = require("sockjs");
const events = require("events");

var logger = require('./utils/logger')("socket");
const logger = require("./utils/logger")("socket");

var services = {};
const services = {};

var init = function(server, config) {
var socket = sockjs.createServer({
log: logger.log.bind(logger)
/**
* Initialize the socket server.
* @param {Object} server - The HTTP server instance.
* @param {Object} config - Configuration object.
*/
const init = (server, config) => {
const socket = sockjs.createServer({
log: logger.log.bind(logger),
});

socket.on('connection', function(conn) {
var service = (conn.pathname.split("/"))[2];
logger.log("connection to service '"+service+"'");
socket.on("connection", (conn) => {
const service = conn.pathname.split("/")[2]; // Extract service name from URL.
logger.log(`Connection to service '${service}'`);

// Check if the service exists.
if (!services[service]) {
conn.close(404, "Service not found");
return logger.error("invalid service '"+service+"'");
return logger.error(`Invalid service '${service}'`);
}

conn.do = function(method, data) {
this.write(JSON.stringify({
'method': method,
'data': data
}));
}.bind(conn);
// Attach a helper method to send data.
conn.do = (method, data) => {
conn.write(
JSON.stringify({
method,
data,
})
);
};

conn.on("data", function(data) {
conn.on("data", (data) => {
// Parse incoming data.
try {
data = JSON.parse(data);
} catch(e) {
logger.error("error parsing data:", data);
return;
} catch (e) {
logger.error("Error parsing data:", data, e.message);
return conn.do("error", { message: "Invalid JSON format" });
}

// Check if the data contains a method.
if (data.method) {
conn.emit("do."+data.method, data.data || {});
conn.emit(`do.${data.method}`, data.data || {});
} else {
conn.emit("message", data);
}
});

// Call the service handler.
services[service].handler(conn);
});

// Install socket handlers with a proper regex prefix.
socket.installHandlers(server, {
prefix: '^/socket/(\\w+)'
prefix: "/socket/\\w+",
});
};

var addService = function(name, handler) {
logger.log("add service", name);
/**
* Add a new service to the socket server.
* @param {string} name - The name of the service.
* @param {Function} handler - The handler function for the service.
*/
const addService = (name, handler) => {
if (
!name ||
typeof name !== "string" ||
!handler ||
typeof handler !== "function"
) {
throw new Error("Invalid service name or handler function.");
}
logger.log("Adding service:", name);

services[name] = {
handler: handler
handler,
};
};


module.exports = {
init: init,
service: addService
init,
service: addService,
};