Skip to content

Commit

Permalink
feat: code interpreter (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
stifskere authored Mar 24, 2024
1 parent e331559 commit 89bea28
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 3 deletions.
31 changes: 31 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 136 additions & 0 deletions src/commands/code-interpreter/slash/RunCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {SlashCommand} from "../../../modules/handlers/HandlerBuilders.js";
import {Attachment, EmbedBuilder, SlashCommandBuilder} from "discord.js";
import {CreateRunnerResponse, GetRunnerDetails} from "../utils/ApiTypes.js";
import {checkRunnerDetails, checkRunnerStatus, getResultEmbed, extensions} from "../utils/RunnerUtils.js";

export default new SlashCommand({
builder: new SlashCommandBuilder()
.setName("run-code")
.setDescription("Code running category")
.addSubcommand(c => c
.setName("from-file")
.setDescription("Run code from a file you upload")
.addAttachmentOption(o => o
.setName("file")
.setDescription("The file to run the code from, the language will be inferred from the file extension")
.setRequired(true)
)
.addStringOption(o => o
.setName("stdin")
.setDescription("Pass input to the code interpreter.")
)
)
.addSubcommand(c => c
.setName("from-input")
.setDescription("Run code directly from your command input")
.addStringOption(o => o
.setName("code")
.setDescription("The script to run.")
.setRequired(true)
)
.addStringOption(o => o
.setName("language")
.setDescription("The coding language the script is written on")
.setRequired(true)
.addChoices(
{name: "C | C17/clang10", value: "c"},
{name: "C++ | C++17/clang10", value: "cpp"},
{name: "Java | OpenJDK18", value: "java"},
{name: "Kotlin | 1.7.10/JRE18", value: "kotlin"},
{name: "Swift | 5.6.2", value: "swift"},
{name: "C# | Mono 6", value: "csharp"},
{name: "Go | 1.19", value: "go"},
{name: "Python 3 | 3.8.10", value: "python3"},
{name: "Ruby | 3.1.2p20", value: "ruby"},
{name: "PHP | 8.1.9 cli", value: "php"},
{name: "BASH | 5.0.17", value: "bash"},
{name: "RLang | 3.6.3", value: "r"},
{name: "JavaScript | NodeJS 16.17.0", value: "javascript"},
{name: "Visual Basic | Mono 6", value: "vb"},
{name: "BrainFuck", value: "brainfuck"},
{name: "Cobol | cobc 2.2.0", value: "cobol"},
{name: "F# | F# Interactive 4.0", value: "fsharp"},
{name: "Elixir | 1.12.3", value: "elixir"},
{name: "Rust | rustc 1.59.0", value: "rust"},
{name: "TypeScript | 4.8.2", value: "typescript"}
)
)
.addStringOption(o => o
.setName("stdin")
.setDescription("Pass input to the code interpreter.")
)
),

async handler(): Promise<void> {
let code: string;
let language: string;
const stdin: string | null = this.context.options.getString("stdin");

if (this.context.options.getSubcommand() === "from-file") {
const file: Attachment = this.context.options.getAttachment("file")!;
code = await fetch(file.url).then(f => f.text());

const splitName: string[] = file.name.split('.');

if (splitName.length < 2
|| !Object.keys(extensions).includes(splitName[splitName.length - 1])) {

const unknownLangEmbed: EmbedBuilder = new EmbedBuilder()
.setTitle("Invalid language")
.setDescription("Inferred language is not valid, valid languages include:\n\n```" +
Object.keys(extensions).map(e => `*.${e}`).join(", ") +
"```"
)
.setColor("#FF0000");

await this.context.reply({
embeds: [unknownLangEmbed],
ephemeral: true
});
return;
}

language = extensions[splitName[splitName.length - 1]];
} else {
code = this.context.options.getString("code")!;
language = this.context.options.getString("language")!;
}

await this.context.deferReply();

const createRunnerResult: CreateRunnerResponse = await fetch("http://api.paiza.io/runners/create?"
+ `source_code=${encodeURIComponent(code)}&`
+ `language=${encodeURIComponent(language)}&`
+ "api_key=guest&"
+ (stdin !== null ? `input=${encodeURIComponent(stdin)}` : ""),
{ method: "POST" }
)
.then(r => r.json());

setTimeout(async (): Promise<void> => {
const checkRunnerResult: CreateRunnerResponse = await checkRunnerStatus(createRunnerResult.id);

if (checkRunnerResult.status === "running") {
const timeoutEmbed: EmbedBuilder = new EmbedBuilder()
.setTitle("Long running session!")
.setDescription(
"Looks like this session is going to take some time," +
"but don't worry you can still use the </run-code get-output:1221193746209701920>" +
"command and check the output for this session once it's finished!"
)
.addFields(
{name: "Session ID", value: checkRunnerResult.id, inline: true},
{name: "Status", value: checkRunnerResult.status, inline: true}
)
.setColor("#FFA500");

await this.context.editReply({embeds: [timeoutEmbed]});
return;
}

const getRunnerResult: GetRunnerDetails = await checkRunnerDetails(createRunnerResult.id);

await this.context.editReply({embeds: [getResultEmbed(getRunnerResult)]});
}, 3000);
}
});
49 changes: 49 additions & 0 deletions src/commands/code-interpreter/slash/SessionResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {SlashCommand} from "../../../modules/handlers/HandlerBuilders.js";
import {EmbedBuilder, SlashCommandBuilder} from "discord.js";
import {CreateRunnerResponse} from "../utils/ApiTypes.js";
import {checkRunnerDetails, checkRunnerStatus, getResultEmbed} from "../utils/RunnerUtils.js";

export default new SlashCommand({
builder: new SlashCommandBuilder()
.setName("code-session")
.setDescription("Get data from code sessions")
.addSubcommand(c => c
.setName("get-result")
.setDescription("Get result from a long running session")
.addStringOption(o => o
.setName("id")
.setDescription("The session ID")
.setRequired(true)
)
),

async handler(): Promise<void> {
await this.context.deferReply();
const sessionId: string = this.context.options.getString("id")!;

const status: CreateRunnerResponse = await checkRunnerStatus(sessionId);

if (status.status === "running") {
const timeoutEmbed: EmbedBuilder = new EmbedBuilder()
.setTitle("On it!")
.setDescription(
"This runner is still running," +
"please, wait and run this command again in a few seconds"
)
.addFields(
{name: "Session ID", value: status.id, inline: true},
{name: "Status", value: status.status, inline: true}
)
.setColor("#FFA500")

await this.context.editReply({
embeds: [timeoutEmbed]
});
return;
}

await this.context.editReply({
embeds: [getResultEmbed(await checkRunnerDetails(sessionId))]
});
}
})
19 changes: 19 additions & 0 deletions src/commands/code-interpreter/utils/ApiTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

export interface CreateRunnerResponse {
id: string;
status: "running" | "completed";
}

export interface GetRunnerDetails {
id: string;

build_stderr: string | null;
build_stdout: string | null;
build_exit_code: number;
build_result: "success" | "failure" | "error";

stdout: string | null;
stderr: string | null;
result: "success" | "failure" | "error";
exit_code: number;
}
59 changes: 59 additions & 0 deletions src/commands/code-interpreter/utils/RunnerUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {CreateRunnerResponse, GetRunnerDetails} from "./ApiTypes.js";
import {EmbedBuilder} from "discord.js";

export const extensions: Record<string, string> = {
"c": "c",
"cpp": "cpp",
"java": "java",
"kt": "kotlin",
"swift": "swift",
"cs": "csharp",
"go": "go",
"py": "python3",
"rb": "ruby",
"php": "php",
"sh": "bash",
"r": "r",
"js": "javascript",
"vb": "vb",
"bf": "brainfuck",
"cob": "cobol",
"fs": "fsharp",
"ex": "elixir",
"rs": "rust",
"ts": "typescript"
}

export async function checkRunnerDetails(id: string): Promise<GetRunnerDetails> {
return await fetch(`http://api.paiza.io/runners/get_details?id=${encodeURIComponent(id)}&api_key=guest`)
.then(r => r.json());
}

export async function checkRunnerStatus(id: string): Promise<CreateRunnerResponse> {
return await fetch(`http://api.paiza.io/runners/get_status?id=${encodeURIComponent(id)}&api_key=guest`)
.then(r => r.json());
}

export function getResultEmbed(details: GetRunnerDetails): EmbedBuilder {
if (details.build_result !== "success" && details.build_result !== null) {
return new EmbedBuilder()
.setTitle("Build error")
.setDescription(`Session ID: \`${details.id}\`\n\`\`\`${details.build_stderr ?? details.build_stdout}\`\`\``)
.setColor("#FF0000")
.setFooter({text: `Exited with code: ${details.build_exit_code}`});
}

if (details.result !== "success") {
return new EmbedBuilder()
.setTitle("Runtime error")
.setDescription(`Session ID: \`${details.id}\`\n\`\`\`${details.stderr ?? details.stdout}\`\`\``)
.setColor("#FF0000")
.setFooter({text: `Exited with code: ${details.exit_code}`});
}

return new EmbedBuilder()
.setTitle("Success")
.setDescription(`Session ID: \`${details.id}\`\n\`\`\`${details.stdout}\`\`\``)
.setColor("#00B000")
.setFooter({text: `Exited with code: ${details.exit_code}`});
}
12 changes: 9 additions & 3 deletions src/modules/handlers/HandlerParameters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {Events, Interaction, SlashCommandBuilder} from "discord.js";
import {BaseContext, ButtonContext, CommandContext, EventContext, SelectMenuContext} from "./HandlerContext.js";
import {Events, Interaction, SlashCommandSubcommandsOnlyBuilder} from "discord.js";
import {
BaseContext,
ButtonContext,
CommandContext,
EventContext,
SelectMenuContext
} from "./HandlerContext.js";

export interface BaseParameters<TThis extends BaseContext> {
handler: (this: TThis, ...params: any[]) => Promise<void> | void;
Expand All @@ -8,7 +14,7 @@ export interface BaseParameters<TThis extends BaseContext> {
export interface CommandParameters<TInteraction extends Interaction>
extends BaseParameters<CommandContext<TInteraction>> {

builder: SlashCommandBuilder;
builder: SlashCommandSubcommandsOnlyBuilder;
}

export interface EventParameters
Expand Down

0 comments on commit 89bea28

Please sign in to comment.