implemented integrations and events system

This commit is contained in:
2026-01-01 16:04:04 -05:00
parent d962b88d73
commit 318762e1b4
7 changed files with 120 additions and 24 deletions

View File

@@ -21,7 +21,10 @@ CLIENT_URL= # This is whatever URL the client web app is served on
CLIENT_DOMAIN= #whatever.com
APPLICATION_VERSION= # Should match release tag
APPLICATION_ENVIRONMENT= # dev / prod
CONFIG_ID= # configures
CONFIG_ID= # config version
# webhooks/integrations
DISCORD_APPLICATIONS_WEBHOOK
# Logger
LOG_DEPTH= # normal / verbose / profiling

View File

@@ -83,6 +83,11 @@ const sessionOptions: session.SessionOptions = {
cookie: cookieOptions
}
import { initializeDiscordIntegrations } from './services/integrations/discord';
//event bus setup
initializeDiscordIntegrations();
app.use(session(sessionOptions));
app.use(passport.authenticate('session'));

View File

@@ -12,6 +12,7 @@ import { Request, response, Response } from 'express';
import { getUserRoles } from '../services/db/rolesService';
import { requireLogin, requireRole } from '../middleware/auth';
import { logger } from '../services/logging/logger';
import { bus } from '../services/events/eventBus';
//get CoC
router.get('/coc', async (req: Request, res: Response) => {
@@ -45,36 +46,24 @@ router.get('/coc', async (req: Request, res: Response) => {
// POST /application
router.post('/', [requireLogin], async (req, res) => {
router.post('/', [requireLogin], async (req: Request, res: Response) => {
const memberID = req.user.id;
const App = req.body?.App || {};
const appVersion = 1;
try {
req.profiler?.start('createApplication');
await createApplication(memberID, appVersion, JSON.stringify(App));
req.profiler?.end('createApplication');
let appID = await createApplication(memberID, appVersion, JSON.stringify(App));
req.profiler?.start('setUserState');
await setUserState(memberID, MemberState.Applicant);
req.profiler?.end('setUserState');
res.sendStatus(201);
// Log full route profiling
const summary = req.profiler?.summary();
if (summary) {
logger.info(
'profiling',
'POST /application completed',
{
memberID,
appVersion,
...summary,
},
'profiling'
);
}
bus.emit("application.create", { application: appID, member_name: req.user.name, member_discord_id: req.user.discord_id || null })
logger.info('app', 'Application Posted', {
user: memberID,
app: appID
})
} catch (err) {
logger.error(
'app',

View File

@@ -200,7 +200,7 @@ passport.deserializeUser(function (user, cb) {
t = performance.now();
const userResults = await con.query(
`SELECT id, name FROM members WHERE id = ?;`,
`SELECT id, name, discord_id FROM members WHERE id = ?;`,
[memberID]
);
timings.memberQuery = performance.now() - t;
@@ -210,6 +210,7 @@ passport.deserializeUser(function (user, cb) {
name: string;
roles: Role[];
state: MemberState;
discord_id?: string;
} = userResults[0];
t = performance.now();
@@ -259,6 +260,7 @@ declare global {
user: {
id: number;
name: string;
discord_id: string;
roles: Role[];
state: MemberState;
};

View File

@@ -2,10 +2,19 @@ import { ApplicationListRow, ApplicationRow, CommentRow } from "@app/shared/type
import pool from "../../db";
import { error } from "console";
export async function createApplication(memberID: number, appVersion: number, app: string) {
/**
* Create an application in the db
* @param memberID
* @param appVersion
* @param app
* @returns ID of the created application
*/
export async function createApplication(memberID: number, appVersion: number, app: string): Promise<number> {
const sql = `INSERT INTO applications (member_id, app_version, app_data) VALUES (?, ?, ?);`;
const params = [memberID, appVersion, JSON.stringify(app)]
return await pool.query(sql, params);
let result = await pool.query(sql, params);
return Number(result.insertId);
}
export async function getMemberApplication(memberID: number): Promise<ApplicationRow> {

View File

@@ -0,0 +1,56 @@
import { randomUUID } from "crypto";
import { logger } from "../logging/logger";
interface Event {
id: string
type: string
occurredAt: string
payload?: Record<string, any>
}
type EventHandler = (event: Event) => void | Promise<void>;
class EventBus {
private handlers: Map<string, EventHandler[]> = new Map();
/**
* Register event listener
* @param type
* @param handler
*/
on(type: string, handler: EventHandler) {
const handlers = this.handlers.get(type) ?? [];
handlers.push(handler);
this.handlers.set(type, handlers);
}
/**
* Emit event of given type
* @param type
* @param payload
*/
async emit(type: string, payload?: Record<string, any>) {
const event: Event = {
id: randomUUID(),
type,
occurredAt: new Date().toISOString(),
payload
}
const handlers = this.handlers.get(type) ?? []
for (const h of handlers) {
try {
await h(event)
} catch (error) {
logger.error('app', 'Event handler failed', {
type: event.type,
id: event.id,
error: error instanceof Error ? error.message : String(error),
})
}
}
}
}
export const bus = new EventBus();

View File

@@ -0,0 +1,32 @@
import { bus } from "../events/eventBus";
export function initializeDiscordIntegrations() {
bus.on('application.create', async (event) => {
let applicantName = event.payload.member_discord_id || event.payload.member_name;
if (event.payload.member_discord_id) {
applicantName = `<@${event.payload.member_discord_id}>`;
}
const link = `${process.env.CLIENT_URL}/administration/applications/${event.payload.application}`;
const embed = {
title: "Application Posted",
description: `[View Application](${link})`,
color: 0x00ff00, // optional: green color
timestamp: new Date().toISOString(), // <-- Discord expects ISO8601
fields: [
{
name: "Submitted By",
value: applicantName,
inline: false,
},
],
};
// send to Discord webhook
await fetch(process.env.DISCORD_APPLICATIONS_WEBHOOK!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] }),
});
});
}