Skip to content

Commit

Permalink
validate all options passed to ptal
Browse files Browse the repository at this point in the history
  • Loading branch information
mandar1jn committed Apr 6, 2024
1 parent 417fc38 commit dd71fb3
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 10 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
"scripts": {
"start": "wrangler dev",
"dev": "wrangler dev",
"register": "bun ./src/register.ts"
"register": "bun ./src/register.ts"
},
"engines": {
"node": ">=18.18.0"
},
"dependencies": {
"@discordjs/builders": "^1.7.0",
"@discordjs/rest": "^2.2.0",
"@octokit/rest": "^20.1.0",
"discord-api-types": "^0.37.79",
"discord-interactions": "^3.4.0",
"dotenv": "^16.4.5",
Expand Down
3 changes: 3 additions & 0 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ export class DiscordClient {
env: Env;
ctx: ExecutionContext;

static client: DiscordClient;

constructor(commands: Command[], env: Env, ctx: ExecutionContext)
{
this.commands = commands;
this.env = env;
this.ctx = ctx;
DiscordClient.client = this;
}

async handle(request: Request): Promise<DiscordResponse | Response>
Expand Down
89 changes: 88 additions & 1 deletion src/client/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,54 @@
import { APIApplicationCommandInteraction, APIBaseInteraction, InteractionResponseType, InteractionType } from "discord-api-types/v10";
import { APIApplicationCommandInteraction, APIApplicationCommandInteractionDataOption, APIApplicationCommandInteractionDataStringOption, APIBaseInteraction, ApplicationCommandOptionType, ApplicationCommandType, InteractionResponseType, InteractionType, MessageFlags } from "discord-api-types/v10";
import { DiscordResponse } from "./response";
import { DiscordClient } from "client";

class InteractionOptions
{
options: APIApplicationCommandInteractionDataOption[] | undefined;

constructor(options: APIApplicationCommandInteractionDataOption[] | undefined)
{
this.options = options;
}

getStringOption(name: string)
{
if(!this.options)
{
return undefined;
}

const option = this.options.find(option => option.name == name && option.type == ApplicationCommandOptionType.String);

if(!option)
{
return undefined;
}

return option as APIApplicationCommandInteractionDataStringOption;
}

getString(name: string)
{
const option = this.getStringOption(name);

if(!option)
{
return undefined;
}
return option.value;
}
}

type DeferOptions = {
hidden: boolean;
}

export class SlashCommandInteraction
{
interaction: APIApplicationCommandInteraction;
options: InteractionOptions;
deferred: boolean = false;

constructor(interaction: APIBaseInteraction<InteractionType, any>)
{
Expand All @@ -13,11 +58,53 @@ export class SlashCommandInteraction
}

this.interaction = interaction as APIApplicationCommandInteraction;

if(this.interaction.data.type != ApplicationCommandType.ChatInput)
{
throw new Error("Invalid application command type. Expected ChatInput")
}

this.options = new InteractionOptions(this.interaction.data.options);

this.interaction.data.options
}

deferReply(options?: DeferOptions, promise?: () => Promise<any>): DiscordResponse
{
this.deferred = true;

if(promise)
{
DiscordClient.client.ctx.waitUntil(promise())
}

let data: any = {};

if(options?.hidden)
{
data.flags = MessageFlags.Ephemeral;
}

return new DiscordResponse(InteractionResponseType.DeferredChannelMessageWithSource, data);
}

reply(data: any)
{
return new DiscordResponse(InteractionResponseType.ChannelMessageWithSource, data);
}

error(error: string)
{
const message = `An internal error occured: \"${error}\"\n\nIf this issues keeps occuring, please contact us.`;

return this.reply({content: message, flags: MessageFlags.Ephemeral});
}

warning(warning: string)
{
const message = `The provided options resulted in the following warning: "${warning}". If you believe this to be an error, please contact us.`;

return this.reply({content: message, flags: MessageFlags.Ephemeral});
}

}
5 changes: 1 addition & 4 deletions src/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Command } from "types";
import {EmbedBuilder, SlashCommandBuilder} from "@discordjs/builders";
import { DiscordResponse } from "client";
import { InteractionResponseType } from "discord-api-types/v10";


const command = {
data: new SlashCommandBuilder()
.setName("help")
.setDescription("help me!"),
execute(interaction) {
async execute(interaction) {

const embed = new EmbedBuilder()
.setTitle("L skillissue")
Expand Down
6 changes: 4 additions & 2 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Command } from "types";
import HelpCommand from "./help"
import HelpCommand from "./help";
import PTALCommand from "./ptal";

const commands: Command[] = [
HelpCommand
HelpCommand,
PTALCommand
];

export default commands;
103 changes: 103 additions & 0 deletions src/commands/ptal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Command } from "types";
import {EmbedBuilder, SlashCommandBuilder} from "@discordjs/builders";
import { Octokit } from "@octokit/rest";

const command = {
data: new SlashCommandBuilder()
.setName("ptal")
.setDescription('Open a Please Take a Look (PTAL) request')
.addStringOption((option) =>
option.setName('github').setDescription('A link to a GitHub pull request').setRequired(true)
)
.addStringOption((option) =>
option.setName('description').setDescription('A short description of the PTAL request').setRequired(true)
)
.addStringOption((option) =>
option.setName('deployment').setDescription('A link to a (preview) deployment related to the PTAL').setRequired(false)
)
.addStringOption((option) =>
option.setName('other').setDescription('Other links related to the PTAL, comma seperated').setRequired(false)
),
async execute(interaction) {

const github = interaction.options.getString("github");
if(!github)
{
return interaction.error("github option required, but not found");
}
const description = interaction.options.getString("description");
if(!description)
{
return interaction.error("github option required, but not found");
}
const deployment = interaction.options.getString("deployment");
let deploymentURL: URL | undefined;
if(deployment)
{
try
{
deploymentURL = new URL(deployment);
}
catch
{
return interaction.warning("Failed to parse the deployment url. Are you sure it was in the correct format? Please sure they start with a valid protocol.");
}
}
const other = interaction.options.getString("other");
let otherURLs: URL[] = [];
if(other)
{
let urls = other.split(",");

urls.forEach(url => {
try
{
const parsedURL = new URL(url.trim());

otherURLs.push(parsedURL);
}
catch
{
return interaction.warning(`Failed to parse the other urls (specifically: ${url}). Are you sure it they were all in the correct format? Please sure they start with a valid protocol.`);
}
})
}

const octokit: Octokit = new Octokit();

const githubRE =
/((https:\/\/)?github\.com\/)?(?<ORGANISATION>[^\/]+)\/(?<REPOSITORY>[^\/]+)\/pull\/(?<NUMBER>\d+)/;
const otherRE = /((?<ORGANISATION>[^\/]+)\/)?(?<REPOSITORY>[^(#|\s|\/)]+)(#)(?<NUMBER>\d+)/;

const match = github.match(githubRE) || github.match(otherRE);
if (!match) {
return interaction.warning("The github PR entered wasn't in a supported format. For help with the format, use /help");
}

let groups = match.groups!;

const pr_info = {
owner: groups['ORGANISATION'],
repo: groups['REPOSITORY'],
pull_number: parseInt(groups['NUMBER']),
};

let pr: Awaited<ReturnType<typeof octokit.rest.pulls.get>>;

try
{
pr = await octokit.rest.pulls.get(pr_info);
}
catch(err)
{
interaction.error("Failed to request pull request from the github api");
}

return interaction.deferReply({hidden: false}, async () => {


});
},
} satisfies Command;

export default command;
4 changes: 2 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import {SlashCommandBuilder} from "@discordjs/builders";
import { DiscordResponse, SlashCommandInteraction } from "client";

declare type Command = {
data: SlashCommandBuilder,
execute(interaction: SlashCommandInteraction): DiscordResponse
data: Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">,
execute(interaction: SlashCommandInteraction): Promise<DiscordResponse>
};

0 comments on commit dd71fb3

Please sign in to comment.