mirror of
https://github.com/sern-handler/automata
synced 2026-06-27 18:22:15 +00:00
275 lines
8.7 KiB
TypeScript
275 lines
8.7 KiB
TypeScript
import express from 'express';
|
|
import 'dotenv/config';
|
|
import { execa } from 'execa';
|
|
import validateJsonWebhook from './plugins/validateJsonWebhook.js';
|
|
import { FeedbackRequestBody, FeedbackRequestBodySchema, Logs } from './util/types.js';
|
|
import cors from 'cors'
|
|
import rateLimit from 'express-rate-limit';
|
|
import { Webhook } from 'simple-discord-webhooks';
|
|
import { codeBlock } from './util/discordCodeBlock.js';
|
|
import db, { schema } from 'database/dist/index.js';
|
|
import jobs from './jobs.js';
|
|
import expressWs from 'express-ws';
|
|
import resolvePlugins from './util/resolvePlugins.js';
|
|
import { stripIndents } from 'common-tags';
|
|
|
|
const devMode = process.argv[2] === '--dev';
|
|
if (devMode) console.log('You\'re a developer 😎 (sorry for that emoji jumpscare)')
|
|
const cwd = process.cwd()
|
|
|
|
console.log('Setting correct permissions for Github SSH key...')
|
|
await execa('chmod', ['-R', '400', '/ssh'], { shell: true })
|
|
console.log('Permissions done!')
|
|
|
|
const { app } = expressWs(express())
|
|
app.use(express.json())
|
|
app.use(cors())
|
|
|
|
const feedbackRateLimit = rateLimit({
|
|
windowMs: 5 * 60 * 1000,
|
|
max: 10,
|
|
standardHeaders: 'draft-7',
|
|
legacyHeaders: true,
|
|
})
|
|
|
|
app.get('/', (req, res) => {
|
|
res.send('hi this is the api what did you even expect')
|
|
})
|
|
|
|
let jobRunning = false
|
|
for (const job of jobs) {
|
|
switch (job.method) {
|
|
case "POST":
|
|
app.post(job.route, async (req, res) => {
|
|
await expressCode(req, res);
|
|
});
|
|
break;
|
|
case "GET":
|
|
app.get(job.route, async (req, res) => {
|
|
await expressCode(req, res);
|
|
});
|
|
break;
|
|
}
|
|
|
|
const expressCode = async (req: express.Request, res: express.Response) => {
|
|
const pluginsResult = await resolvePlugins(job.plugins, req, res);
|
|
if (pluginsResult.includes(false)) {
|
|
return res.status(418).send({ success: false, message: "Plugins didn't pass" });
|
|
}
|
|
|
|
res.send({ success: true, message: "Command is running" });
|
|
jobRunning = true;
|
|
|
|
const parse_payload = (level: 'info' | 'error', payload: any) => ({
|
|
timestamp: new Date(),
|
|
message: payload.toString(),
|
|
level
|
|
})
|
|
|
|
const jobLogs = [] as { step: number, logs: Logs[] }[]
|
|
let jobSuccessful = true
|
|
|
|
try {
|
|
for (let i = 0; i < job.steps.length; i++) {
|
|
const steps = job.steps[i]!;
|
|
const logsToPush = [] as Logs[];
|
|
|
|
console.log(`Running step ${steps.name}`);
|
|
|
|
const cmd = execa(
|
|
"bash",
|
|
[`${cwd}/scripts/${job.stepsMainDir}/${steps.script}`],
|
|
{
|
|
cwd: steps.cwd.startsWith('repos/') ? `${cwd}/../../${steps.cwd}` : steps.cwd,
|
|
shell: true,
|
|
env: {
|
|
NT_ARGS: JSON.stringify({
|
|
...job.cmdArgs,
|
|
requestBody: req.body,
|
|
})
|
|
},
|
|
},
|
|
)
|
|
cmd.stdout!.on('data', (data) => logsToPush.push(parse_payload('info', data.toString().replace(/\n$/, ""))));
|
|
cmd.stderr!.on('data', (data) => logsToPush.push(parse_payload('error', data.toString().replace(/\n$/, ""))));
|
|
await new Promise((resolve, reject) => {
|
|
cmd.once('exit', (code) => {
|
|
if (code === 0 || !code) {
|
|
console.log(`Step ${steps.name} finished successfully`);
|
|
logsToPush.push(parse_payload('info', 'Step finished successfully'));
|
|
jobLogs.push({ step: steps.id, logs: logsToPush });
|
|
jobSuccessful = true;
|
|
resolve('nice');
|
|
} else {
|
|
console.log(`Step ${steps.name} failed with code ${code}`);
|
|
logsToPush.push(parse_payload('error', `Step failed with code ${code}`));
|
|
jobLogs.push({ step: steps.id, logs: logsToPush });
|
|
jobSuccessful = false;
|
|
reject('stop it');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch {}
|
|
jobRunning = false
|
|
|
|
const markdownText = stripIndents`
|
|
# Job ${job.name} finished
|
|
## Steps
|
|
${jobLogs.map((step) => {
|
|
return `### Step ${step.step}: ${job.steps.find(s => s.id === step.step)?.name}\n\`\`\`\n${step.logs.map((log) => {
|
|
return `${log.timestamp.toISOString()} - ${log.level.toUpperCase()} | ${log.message}`;
|
|
}).join('\n')}\n\`\`\``;
|
|
}).join('\n')}
|
|
`;
|
|
|
|
const createSnippet = await fetch(`${process.env.SERN_BIN_ENDPOINT}/api/create`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': process.env.SERN_BIN_KEY!,
|
|
},
|
|
body: JSON.stringify({
|
|
fileName: `run-${job.name}-${new Date().toISOString()}.md`,
|
|
description: `Logs for ${job.name} job`,
|
|
authorId: process.env.SERN_BIN_USER,
|
|
lang: "markdown",
|
|
code: markdownText
|
|
})
|
|
}).then(async res => (await res.text()).replaceAll('"', ''))
|
|
const dbWrite = (await db.insert(schema.jobsList).values({
|
|
name: job.name,
|
|
steps: job.steps,
|
|
sernbinid: createSnippet
|
|
}).returning())[0]
|
|
|
|
const webhook = new Webhook(new URL(process.env.AUTOMATA_CHANNEL_WEBHOOK!), 'Job Logs (by automata)', 'https://avatars.githubusercontent.com/u/129876409?v=4')
|
|
webhook.send(`Job #${dbWrite?.id} ${job.name} finished`, [{
|
|
color: jobSuccessful ? 0x00ff00 : 0xff0000,
|
|
description: `Job ${job.name} finished with ${jobSuccessful ? 'no errors' : 'errors'}`,
|
|
fields: [
|
|
{ name: 'Snippet', value: `[Here](https://bin.sern.dev/s/${createSnippet})`, inline: true },
|
|
],
|
|
}])
|
|
}
|
|
};
|
|
|
|
app.post('/wh/updateDocsJson', async (req, res) => {
|
|
const validate = validateJsonWebhook(req)
|
|
if (!validate) {
|
|
res.status(403).send({
|
|
success: false,
|
|
error: 'Invalid token'
|
|
})
|
|
return
|
|
}
|
|
if (req.body.action !== 'released') {
|
|
res.send({
|
|
success: true,
|
|
error: 'Token valid, but ignoring action...'
|
|
})
|
|
// this line fixed the entire instability of automata
|
|
return;
|
|
}
|
|
const cmd = execa('bash', ['scripts/updateDocsJson.sh', process.env.GHTOKEN!, process.env.EMAIL!], { shell: true })
|
|
cmd.stdout!.on('data', (data) => console.log(JSON.stringify(data.toString())))
|
|
cmd.stderr!.on('data', (data) => console.log(JSON.stringify(data.toString())))
|
|
res.send({
|
|
success: true,
|
|
message: "command is running"
|
|
})
|
|
})
|
|
|
|
app.post('/tutorial/feedback', feedbackRateLimit, async (req, res) => {
|
|
const body = req.body as FeedbackRequestBody
|
|
// validation of request body
|
|
try {
|
|
FeedbackRequestBodySchema.parse(body)
|
|
} catch {
|
|
return res
|
|
.status(400)
|
|
.send({
|
|
successful: false,
|
|
error: "You have something missing in your request!",
|
|
});
|
|
}
|
|
if (body.feedback !== "up" && body.feedback !== "down")
|
|
return res
|
|
.status(400)
|
|
.send({
|
|
successful: false,
|
|
error: "Feedback must be either 'up' or 'down'!",
|
|
});
|
|
if (!body.route.startsWith('/docs/tutorial'))
|
|
return res
|
|
.status(400)
|
|
.send({
|
|
successful: false,
|
|
error: "Are you sure you didn't modify this request?",
|
|
});
|
|
|
|
// part where turnstile token gets validated
|
|
const turnstileFormData = new URLSearchParams()
|
|
turnstileFormData.append('response', body.turnstileToken)
|
|
turnstileFormData.append('secret', process.env.TURNSTILE_SECRET_DEV!)
|
|
const turnstileResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
method: 'POST',
|
|
body: turnstileFormData
|
|
}).then(res => res.json())
|
|
if (!turnstileResponse.success)
|
|
return res
|
|
.status(403)
|
|
.send({
|
|
successful: false,
|
|
error: "Turnstile verificaion not successful",
|
|
});
|
|
|
|
// actual database recording
|
|
const trimmedRoute = body.route.replace('/docs/tutorial', '')
|
|
const data = {
|
|
id: crypto.randomUUID(),
|
|
feedback: body.feedback,
|
|
route: trimmedRoute,
|
|
inputText: body.inputText,
|
|
}
|
|
await db.insert(schema.guideFeedback).values(data).execute()
|
|
res.send({
|
|
successful: true,
|
|
message: "Feedback recorded!",
|
|
});
|
|
|
|
// webhook
|
|
const webhook = new Webhook(new URL(process.env.DEV_WEBHOOK!), 'Guide Feedback (by automata)', 'https://avatars.githubusercontent.com/u/129876409?v=4')
|
|
// const upvoteCount = (await pb.collection('feedback').getFullList({ filter: `feedback = 'up' && route = '${body.route}'` })).length
|
|
// const downvoteCount = (await pb.collection('feedback').getFullList({ filter: `feedback = 'down' && route = '${body.route}'` })).length
|
|
const upvoteCount = (await (db.query.guideFeedback!.findMany({
|
|
where: (guideFeedback, { eq, and }) => and(
|
|
eq(guideFeedback.feedback, 'up'),
|
|
eq(guideFeedback.route, trimmedRoute)
|
|
),
|
|
})).execute()).length
|
|
const downvoteCount = (await (db.query.guideFeedback!.findMany({
|
|
where: (guideFeedback, { eq, and }) => and(
|
|
eq(guideFeedback.feedback, 'down'),
|
|
eq(guideFeedback.route, trimmedRoute)
|
|
),
|
|
})).execute()).length
|
|
webhook.send(`Feedback recorded for ${body.route}!`, [{
|
|
color: body.feedback === 'up' ? 0x00ff00 : 0xff0000,
|
|
description: body.inputText ? codeBlock(body.inputText) : undefined,
|
|
fields: [
|
|
{ name: 'Ratio', value: `${(upvoteCount / (upvoteCount + downvoteCount) * 100).toFixed(2)}%`, inline: true },
|
|
{ name: 'Upvotes', value: upvoteCount.toString(), inline: true },
|
|
{ name: 'Downvotes', value: downvoteCount.toString(), inline: true },
|
|
]
|
|
}])
|
|
})
|
|
|
|
app.get('/ping', (req, res) => {
|
|
res.send('Pong')
|
|
})
|
|
|
|
const port = Number(process.env.PORT!) || 4000
|
|
app.listen(port, '0.0.0.0', () => {
|
|
console.log(`Server listening on [::]${port}`)
|
|
})
|