Merge pull request 'webhooks/integrations' (#142) from webhooks/integrations into main
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m27s
All checks were successful
Continuous Integration / Update Development (push) Successful in 2m27s
Reviewed-on: #142
This commit was merged in pull request #142.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
56
api/src/services/events/eventBus.ts
Normal file
56
api/src/services/events/eventBus.ts
Normal 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();
|
||||
32
api/src/services/integrations/discord.ts
Normal file
32
api/src/services/integrations/discord.ts
Normal 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] }),
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user