Skip to content

Commit

Permalink
Almost fully functioning [0.2.0]
Browse files Browse the repository at this point in the history
  • Loading branch information
leomotors committed Jan 22, 2022
1 parent 3da10ad commit 340b035
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 37 deletions.
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.json
*.yml
.prettierrc
*.md
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,15 @@
A Discord Bot Grader

~~Created to compete with [Cafe Grader](https://github.com/cafe-grader-team/cafe-grader-web)~~

## 🌲 Dependencies

### To Start the Bot

- nodejs (16.6+) with npm, yarn

### To make sure no error occurred

- gcc

- pshved/timeout installed to bin, [see here](https://unix.stackexchange.com/a/44988)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cocoa-grader",
"version": "1.0.0",
"version": "0.2.0",
"description": "Discord Bot Grader",
"main": "index.js",
"repository": "https://github.com/Leomotors/cocoa-grader",
Expand Down
2 changes: 1 addition & 1 deletion src/bot/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ client.login(process.env.DISCORD_TOKEN);

client.on("ready", (cli) => {
console.log(
chalk.green(`ココアお姉ちゃん 「${cli.user.tag}」 は準備完了です`)
chalk.cyan(`ココアお姉ちゃん 「${cli.user.tag}」 は準備完了です`)
);
center.syncCommands();
});
Expand Down
32 changes: 26 additions & 6 deletions src/bot/commands/message/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import { Message } from "discord.js";
import fetch from "node-fetch";
import { problemExists } from "../../../grader/problems";
import Grade from "../../../grader/grader";
import { getLang, SupportedLang } from "../../../grader/compile";

// * Accept Submission and Print Result
export default async function submit(message: Message) {
let problem = message.content.split(" ")[2]?.split("\n")[0];
let problem = message.content
.split(" ")
.filter((s) => s.length > 0)[2]
?.split("\n")[0];

if (!problemExists(problem)) {
message.reply(`That Problem does not exist!`);
await message.reply(`That Problem does not exist!`);
return;
}

let userCode = "";
let userLang = "";

if (message.attachments.size > 0) {
const attachment = message.attachments.first()!;
Expand All @@ -23,24 +28,39 @@ export default async function submit(message: Message) {
try {
const res = await fetch(url);
userCode = await res.text();
const tokens = fname?.split(".");
userLang = tokens?.[tokens.length - 1] ?? "";
} catch (error) {
console.log(chalk.red(`Error Reading File: ${error}`));
}
} else {
const tokens = message.content.split("```");
if (tokens.length != 3) {
message.reply("Invalid Format");
await message.reply("Invalid Format");
return;
}

userCode = tokens[1].split("\n").slice(1).join("\n");
userLang = tokens[1].split("\n")[0];
}

if (userCode.length < 1) {
message.reply("Where Code?");
await message.reply("Where Code?");
return;
}
if (userLang.length < 1) {
await message.reply("Please Specify Language");
return;
}

const lang = getLang(userLang);
if (lang == "Unsupported") {
await message.reply("Unsupported Language");
return;
}

const msg = await message.reply("Grading...");

const result = await Grade(problem, userCode);
message.reply(`${result.status} ${result.score}`);
const result = await Grade(problem, userCode, lang, message.author.tag);
msg.edit(`${result.status} ${result.score} [${result.subtasks}]`);
}
15 changes: 12 additions & 3 deletions src/bot/commands/messageCmd.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import chalk from "chalk";
import { Message } from "discord.js";

import submit from "./message/submit";
Expand All @@ -6,14 +7,22 @@ const commands = {
submit,
};

export function processMessageCommand(message: Message) {
const rawCmd = message.content.split(" ").slice(1).join(" ");
export async function processMessageCommand(message: Message) {
const rawCmd = message.content
.split(" ")
.filter((s) => s.length > 0)
.slice(1)
.join(" ");

const cmd = rawCmd.split(" ")[0];

for (const [cmdName, cmdFunc] of Object.entries(commands)) {
if (cmdName === cmd) {
cmdFunc(message);
try {
await cmdFunc(message);
} catch (error) {
console.log(chalk.red(`Error Processing Command: ${error}`));
}
return;
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/bot/commands/slash/aboutme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CocoaSlash } from "cocoa-discord-utils/slash";
import { SlashCommandBuilder } from "@discordjs/builders";

export const aboutme: CocoaSlash = {
command: new SlashCommandBuilder()
.setName("aboutme")
.setDescription("Get Info about me!")
.toJSON(),
func: async (ctx) => {
await ctx.reply(
`I am Cocoa Grader! Who will carefully grade your code! 💖💖\nVersion: ${process.env.npm_package_version}`
);
},
};
4 changes: 2 additions & 2 deletions src/bot/commands/slash/getstatement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CocoaSlash } from "cocoa-discord-utils/slash";
import { CommandInteraction } from "discord.js";
import { SlashCommandBuilder } from "@discordjs/builders";

import { getProblems } from "../../../grader/problems";

export const getstatement: CocoaSlash = {
Expand All @@ -14,7 +14,7 @@ export const getstatement: CocoaSlash = {
.setRequired(true)
)
.toJSON(),
func: async (ctx: CommandInteraction) => {
func: async (ctx) => {
const problem_name = ctx.options.getString("problem_name", true);
const Problem = getProblems(problem_name);

Expand Down
3 changes: 3 additions & 0 deletions src/bot/commands/slash/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Cog } from "cocoa-discord-utils/slash";

import { aboutme } from "./aboutme";
import { getstatement } from "./getstatement";

export const Cocoa: Cog = {
name: "Cocoa",
commands: {
aboutme,
getstatement,
},
};
56 changes: 56 additions & 0 deletions src/grader/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import chalk from "chalk";
import { writeFile } from "fs/promises";
import { exec } from "./grader";

export type SupportedLang = "C" | "C++" | "Python";

export function getLang(str: string): SupportedLang | "Unsupported" {
if (str == "cpp" || str == "c++" || str == "cc") return "C++";
if (str == "c") return "C";
if (str.startsWith("py")) return "Python";
return "Unsupported";
}

const extensions = {
C: "c",
"C++": "cpp",
Python: "py",
};

export async function Compile(
lang: SupportedLang,
content: string,
id: string
): Promise<boolean> {
try {
await writeFile(`temp/${id}.${extensions[lang]}`, content);
} catch (error) {
console.log(chalk.red(`ERROR while writing file: ${error}`));
return false;
}

try {
switch (lang) {
case "C":
await exec(`gcc temp/${id}.c -o temp/${id} -std=c17 -O2 -lm`);
break;
case "C++":
await exec(
`g++ temp/${id}.cpp -o temp/${id} -std=c++17 -O2 -lm`
);
break;
case "Python":
// Do Nothing Lmao
break;
}
} catch (error) {
return false;
}

return true;
}

export function getECmd(lang: string, id: string) {
if (lang == "Python") return `python3 ./temp/${id}.py`;
return `./temp/${id}`;
}
73 changes: 49 additions & 24 deletions src/grader/grader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,96 @@ import { promisify } from "util";
import { writeFile } from "fs/promises";
import { v4 as uuid } from "uuid";

import { getProblems } from "./problems";
import { getProblems, Problem } from "./problems";
import { wCmp } from "./compare/wcmp";
import { shortenVerdicts } from "./utils";
import { Compile, getECmd, SupportedLang } from "./compile";

const exec = promisify(execCb);
export const exec = promisify(execCb);

export interface Verdict {
status: "Compilation Error" | "Rejected" | "Accepted";
score: number;
subtasks: string;
}

export type CaseVerdict =
| "Correct Answer"
| "Wrong Answer"
| "Time Limit Exceeded"
| "Runtime Error";
export const VerdictDict = {
"Correct Answer": "P",
"Wrong Answer": "-",
"Time Limit Exceeded": "T",
"Runtime Error": "x",
};

export type CaseVerdict = keyof typeof VerdictDict;

export default async function Grade(
problemID: string,
code: string
code: string,
lang: SupportedLang,
submitter: string
): Promise<Verdict> {
const problem = getProblems(problemID);
const submissionId = uuid();

try {
await writeFile(`temp/${submissionId}.cpp`, code);
await exec(
`g++ temp/${submissionId}.cpp -o temp/${submissionId} -std=c++17 -O2 -lm`
);
} catch (error) {
return { status: "Compilation Error", score: 0 };
const tag = `[#${submissionId.split("-")[0]} by ${submitter}]`;
console.log(`${tag} Working on ${problem.title} (Language: ${lang})`);

const res = await Compile(lang, code, submissionId);
if (!res) {
return {
status: "Compilation Error",
score: 0,
subtasks: "E",
};
}

let totalScore = 0;
const subtaskVerdicts: CaseVerdict[] = [];
for (const [subtaskName, subtaskScore] of Object.entries(
problem.subtasks
)) {
const subtaskRes =
(await GradeCase(
`./temp/${submissionId}`,
`./problems/${problemID}/testcase/${subtaskName}`
)) == "Correct Answer";
const subtaskVerdict = await GradeCase(
getECmd(lang, submissionId),
`./problems/${problemID}/testcase/${subtaskName}`,
problem
);

totalScore += subtaskRes ? subtaskScore : 0;
totalScore += subtaskVerdict == "Correct Answer" ? subtaskScore : 0;
subtaskVerdicts.push(subtaskVerdict);
}

const subtaskStr = shortenVerdicts(subtaskVerdicts);
console.log(`${tag} Graded ${problem.title} [${subtaskStr}]`);
return {
status:
totalScore == (problem.maxScore ?? 100) ? "Accepted" : "Rejected",
score: totalScore,
subtasks: subtaskStr,
};
}

async function GradeCase(
execloc: string,
caseloc: string
caseloc: string,
problem: Problem
): Promise<CaseVerdict> {
try {
const runRes = await exec(`cat ${caseloc}.in | ${execloc}`);
const runRes = await exec(
`cat ${caseloc}.in | timeout -t ${problem.timelimit} -m ${
problem.memorylimit * 1000
} ${execloc}`
);

if (runRes.stderr.startsWith("TIMEOUT")) return "Time Limit Exceeded";
if (runRes.stderr.startsWith("MEM")) return "Runtime Error";

if (await wCmp(runRes.stdout, `${caseloc}.out`)) {
return "Correct Answer";
} else {
return "Wrong Answer";
}
} catch (error) {
console.log(error);
console.log(`Error while grading ${caseloc} : ${error}`);
return "Runtime Error";
}
}
6 changes: 6 additions & 0 deletions src/grader/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CaseVerdict, VerdictDict } from "./grader";

export function shortenVerdicts(verdicts: CaseVerdict[]): string {
const verdictsShort = verdicts.map((verdict) => VerdictDict[verdict]);
return verdictsShort.join("");
}

0 comments on commit 340b035

Please sign in to comment.