Add create jira bug

This commit is contained in:
Amaryllis 2023-12-18 14:04:26 +01:00
parent 6c8cf194a7
commit 1a7822ea69
13 changed files with 418 additions and 97 deletions

26
bind-commands.ts Normal file
View file

@ -0,0 +1,26 @@
import { Collection } from "discord.js";
import { client } from "./bot";
import path from 'node:path';
import fs from "fs"
export const bindCommands = async () => {
client.commands = new Collection();
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);
for (const folder of commandFolders) {
const commandsPath = path.join(foldersPath, folder);
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.ts'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
// Set a new item in the Collection with the key as the command name and the value as the exported module
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
console.log(`Bound to ${filePath} command`)
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
}

102
bot.ts
View file

@ -1,7 +1,9 @@
import { Client, GatewayIntentBits, GuildMember, Channel, TextBasedChannel, Events, Message, SlashCommandBuilder, Collection } from 'discord.js';
import { Client, GatewayIntentBits, Collection, ContextMenuCommandBuilder, ApplicationCommandType } from 'discord.js';
import * as dotenv from 'dotenv';
import path from 'node:path';
import fs from 'node:fs'
import { deployCommands } from './deploy-commands';
import { bindCommands } from './bind-commands';
import { bindToEvents } from './utils/bind-events';
import { loadMessages } from './model/messages';
dotenv.config();
@ -11,92 +13,16 @@ declare module "discord.js" {
}
}
import config from "./config";
export const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers] });
export const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] });
const init = async () => {
await deployCommands();
await bindCommands();
import handleMessage from './messageHandler';
import search from './model/jira';
client.login(process.env.token);
client.on("ready", () => {
console.log("successfully logged in");
});
bindToEvents();
loadMessages();
}
client.on("guildMemberAdd", async (member: GuildMember) => {
const msg = `Welcome, <@${member.id}>! If you joined for any specific support questions `
+ `please check out <#863171642905591830> first to see if your issue is known, `
+ `and make sure that your app is up-to-date before posting.`;
const channel: TextBasedChannel | null = await client.channels.fetch(config.channels.joins) as TextBasedChannel;
channel?.send(msg);
});
client.on(Events.MessageCreate, async (msg: Message) => {
if (msg.content.startsWith(".cl ")) {
const version = msg.content.substring(4)
try {
// Test if a number
const versionNumber = Number(version)
const issues = await search(version);
const issueDataList: { title: string, key: string }[] = []
issues.forEach((issue) => {
issueDataList.push({ title: issue.fields.summary, key: issue.key })
})
const issueTextList: string[] = []
issueDataList.forEach((issue) => {
issueTextList.push(`[${issue.key}] ${issue.title}`)
})
let issueIndex = 0
let messagesToSend: string[] = []
let parsedAll = false
while (!parsedAll) {
let formattedMsg = ""
let foundLength = 6
for (let i = issueIndex; i < issueTextList.length + 1; ++i) {
if (i == issueTextList.length) {
parsedAll = true;
break;
}
const line = `${issueTextList[i]}\n`
if (foundLength + line.length > 1999) {
break;
}
formattedMsg += line
foundLength += line.length
issueIndex = i;
}
messagesToSend.push(`\`\`\`${formattedMsg}\`\`\``)
}
let index = 0
messagesToSend.forEach((messageToSend) => {
let waitMultiplier = index + 1
let waitTime = 1000 * waitMultiplier
setTimeout(() => {
msg.channel.send(messageToSend)
}, waitTime);
index++;
})
}
catch (e) {
}
}
else {
await handleMessage(msg).catch(console.error);
}
});
client.login(process.env.token);
init();

134
commands/jira/create-bug.ts Normal file
View file

@ -0,0 +1,134 @@
import axios from "axios";
import { ApplicationCommandType, CommandInteraction, ContextMenuCommandBuilder, ModalBuilder, ModalSubmitInteraction, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
import config from "../../config";
import { frequencies, getJiraToken, severites } from "../../utils/jira";
const { ActionRowBuilder, } = require('discord.js');
const getPlaceholderFromList = (list: { label: string, value: string }[]) => {
let placeholder: string = ''
for (let i = 0; i < list.length; ++i) {
placeholder += `${i + 1}=${list[i].label}\n`
}
return placeholder
}
module.exports = {
data: new ContextMenuCommandBuilder()
.setName('Create Bug')
.setDefaultMemberPermissions(0)
.setType(ApplicationCommandType.Message),
async execute(interaction: CommandInteraction) {
const modal = new ModalBuilder()
.setCustomId('createBugModal')
.setTitle('Create a new bug');
const titleInput = new TextInputBuilder()
.setCustomId('title')
.setLabel("What should we call the bug?")
.setRequired(true)
.setMaxLength(200)
.setStyle(TextInputStyle.Short);
const descInput = new TextInputBuilder()
.setCustomId('desc')
.setLabel("Description/More info")
.setRequired(false)
.setMaxLength(1000)
.setStyle(TextInputStyle.Paragraph);
const frequencyInput = new TextInputBuilder()
.setCustomId('frequency')
.setLabel("How often does it happen?")
.setMaxLength(1)
.setMinLength(1)
.setRequired(true)
.setPlaceholder(getPlaceholderFromList(frequencies))
.setStyle(TextInputStyle.Paragraph);
const severityInput = new TextInputBuilder()
.setCustomId('severity')
.setLabel("How severe is the bug?")
.setMaxLength(1)
.setMinLength(1)
.setRequired(true)
.setPlaceholder(getPlaceholderFromList(severites))
.setStyle(TextInputStyle.Paragraph);
const firstActionRow = new ActionRowBuilder().addComponents(titleInput);
const secondActionRow = new ActionRowBuilder().addComponents(descInput);
const thirdActionRow = new ActionRowBuilder().addComponents(severityInput);
const fourthActionRow = new ActionRowBuilder().addComponents(frequencyInput);
modal.addComponents(firstActionRow, secondActionRow, thirdActionRow, fourthActionRow);
await interaction.showModal(modal);
const result = await interaction.awaitModalSubmit({
time: 60000,
filter: i => i.user.id === interaction.user.id,
}).catch((e) => undefined)
if (result == undefined) {
return;
}
const title = result.fields.getTextInputValue('title')
const frequency = result.fields.getTextInputValue('frequency')
const severity = result.fields.getTextInputValue('severity')
const descrption = result.fields.getTextInputValue('desc')
if (Number.isNaN(Number.parseInt(severity))) {
result.reply({ ephemeral: true, content: `You entered a non-number for severity. Please only submit a number as listed in the placeholder!` })
return;
}
if (Number.isNaN(Number.parseInt(frequency))) {
result.reply({ ephemeral: true, content: `You entered a non-number for frequency. Please only submit a number as listed in the placeholder!` })
return;
}
if (interaction.isMessageContextMenuCommand()) {
const messageUrl = interaction.targetMessage.url
const url = `${config.jira.baseUrl}/issue`;
const issuePayload = {
fields: {
project: {
key: "SP"
},
summary: title,
description: `Issue submitted from Spot in reaction to: ${messageUrl}\n\n${descrption}`,
issuetype: {
id: "10005"
},
customfield_10057: {
id: severites[parseInt(severity) - 1].value
},
customfield_10056: {
id: frequencies[parseInt(frequency) - 1].value
}
}
}
console.log(issuePayload);
const serverResponse = await new axios.Axios({}).post(url, JSON.stringify(issuePayload), { headers: { "Authorization": getJiraToken(), "Content-Type": 'application/json' }, },).catch((e) => console.error(e))
if (serverResponse && serverResponse.status == 201) {
const responseData = JSON.parse(serverResponse.data)
result.reply({ ephemeral: false, content: `A bug report has been created for your issue under issue-ID ${responseData.key}!`, target: interaction.targetMessage, options: { flags: "SuppressNotifications" } })
}
else {
console.log(serverResponse)
result.reply({ ephemeral: true, content: 'Your submission failed. Something went wrong' })
}
}
else {
result.reply({ ephemeral: true, content: 'Unable to find message this bug belongs to' })
}
},
};

12
commands/utility/ping.ts Normal file
View file

@ -0,0 +1,12 @@
import { CommandInteraction } from "discord.js";
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
async execute(interaction: CommandInteraction) {
await interaction.reply('Pong!');
},
};

44
deploy-commands.ts Normal file
View file

@ -0,0 +1,44 @@
import config from "./config";
const { REST, Routes } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');
const dotenv = require('dotenv');
export const deployCommands = async () => {
dotenv.config();
const commands: any[] = [];
const foldersPath = path.join(__dirname, 'commands');
const commandFolders = fs.readdirSync(foldersPath);
for (const folder of commandFolders) {
const commandsPath = path.join(foldersPath, folder);
const commandFiles = fs.readdirSync(commandsPath).filter((file: string) => file.endsWith('.ts'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON());
} else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
}
const rest = new REST().setToken(process.env.token);
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`);
const data = await rest.put(
Routes.applicationGuildCommands(process.env.appid, config.guild),
{ body: commands },
);
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
console.error(error);
}
}

View file

@ -0,0 +1,12 @@
import { GuildMember, TextBasedChannel } from "discord.js";
import { client } from "../bot";
import config from "../config";
export const onGuildMemberAdd = async (member: GuildMember) => {
const msg = `Welcome, <@${member.id}>! If you joined for any specific support questions `
+ `please check out <#863171642905591830> first to see if your issue is known, `
+ `and make sure that your app is up-to-date before posting.`;
const channel: TextBasedChannel | null = await client.channels.fetch(config.channels.joins) as TextBasedChannel;
channel?.send(msg);
}

View file

@ -0,0 +1,32 @@
import axios from "axios";
import { CacheType, ChatInputCommandInteraction, Interaction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, UserContextMenuCommandInteraction } from "discord.js";
import { client } from "../bot";
import config from "../config";
import { frequencies, getJiraToken, severites } from "../utils/jira";
export const onInteractionCreate = async (interaction: Interaction) => {
if (interaction.isChatInputCommand() || interaction.isContextMenuCommand()) {
onCommand(interaction);
}
}
const onCommand = async (interaction: ChatInputCommandInteraction<CacheType> | MessageContextMenuCommandInteraction<CacheType> | UserContextMenuCommandInteraction) => {
const command = client.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
} else {
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
}
}

70
events/message-create.ts Normal file
View file

@ -0,0 +1,70 @@
import { Message } from "discord.js";
import handleMessage from "../messageHandler";
import search from "../model/jira";
export const onMessageCreate = async (msg: Message) => {
if (msg.content.startsWith(".cl ")) {
const version = msg.content.substring(4)
try {
// Test if a number
const versionNumber = Number(version)
const issues = await search(version);
const issueDataList: { title: string, key: string }[] = []
issues.forEach((issue) => {
issueDataList.push({ title: issue.fields.summary, key: issue.key })
})
const issueTextList: string[] = []
issueDataList.forEach((issue) => {
issueTextList.push(`[${issue.key}] ${issue.title}`)
})
let issueIndex = 0
let messagesToSend: string[] = []
let parsedAll = false
while (!parsedAll) {
let formattedMsg = ""
let foundLength = 6
for (let i = issueIndex; i < issueTextList.length + 1; ++i) {
if (i == issueTextList.length) {
parsedAll = true;
break;
}
const line = `${issueTextList[i]}\n`
if (foundLength + line.length > 1999) {
break;
}
formattedMsg += line
foundLength += line.length
issueIndex = i;
}
messagesToSend.push(`\`\`\`${formattedMsg}\`\`\``)
}
let index = 0
messagesToSend.forEach((messageToSend) => {
let waitMultiplier = index + 1
let waitTime = 1000 * waitMultiplier
setTimeout(() => {
msg.channel.send(messageToSend)
}, waitTime);
index++;
})
}
catch (e) {
}
}
else {
await handleMessage(msg).catch(console.error);
}
}

3
events/ready.ts Normal file
View file

@ -0,0 +1,3 @@
export const onReady = () => {
console.log("Bot ready!")
}

View file

@ -1,5 +1,6 @@
import * as axios from 'axios';
import config from '../config';
import { getJiraToken } from '../utils/jira';
export default async function search(version?: string): Promise<any[]> {
const params = {
@ -17,14 +18,19 @@ export default async function search(version?: string): Promise<any[]> {
do {
// @ts-expect-error
const httpParams = Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
const httpParams = Object.keys(params).map((key: string) => `${key}=${params[key]}`).join('&');
const url = `${config.jira.baseUrl}/search?${httpParams}`;
const serverResponse = await new axios.Axios({}).get(url);
jsonResponse = JSON.parse(serverResponse.data);
const serverResponse = await new axios.Axios({}).get(url, { headers: { "Authorization": getJiraToken() } }).catch((e) => console.error(e))
if (serverResponse) {
jsonResponse = JSON.parse(serverResponse.data);
issues.push(...jsonResponse!.issues);
params.startAt += 100;
}
else {
return []
}
issues.push(...jsonResponse!.issues);
params.startAt += 100;
} while (jsonResponse!.issues.length > 99 && jsonResponse!.total > 100);
return issues;

View file

@ -12,7 +12,7 @@ interface Message {
export const messages: Record<string, Message> = {};
let avatarUrl = "";
const load = async () => {
export const loadMessages = async () => {
const messagesList = require('../messages.json')["faq"];
messagesList.forEach((msg: any) => {
messages[msg["title"]] = msg
@ -26,9 +26,6 @@ const load = async () => {
console.log(avatarUrl);
}
load();
export const get = (name: string) => messages[name];
export const getList = () => Object.keys(messages).map(msg => ({ name: messages[msg].names[0], value: messages[msg].description }));

13
utils/bind-events.ts Normal file
View file

@ -0,0 +1,13 @@
import { Events } from "discord.js";
import { client } from "../bot";
import { onGuildMemberAdd } from "../events/guild-member-add";
import { onInteractionCreate } from "../events/interaction-create";
import { onMessageCreate } from "../events/message-create";
import { onReady } from "../events/ready";
export const bindToEvents = () => {
client.on(Events.GuildMemberAdd, onGuildMemberAdd);
client.on(Events.MessageCreate, onMessageCreate);
client.on(Events.ClientReady, onReady);
client.on(Events.InteractionCreate, onInteractionCreate)
}

46
utils/jira.ts Normal file
View file

@ -0,0 +1,46 @@
export const getJiraToken = () => {
const token = Buffer.from(`${process.env.jiramail}:${process.env.jiratoken}`).toString('base64')
return `Basic ${token}`
}
export const frequencies = [
{
label: "Everytime (everyone)",
value: "10023"
},
{
label: "Sometimes (everyone)",
value: "10024"
},
{
label: "Everytime (some)",
value: "10025"
},
{
label: "Everyime/Always (one user)",
value: "10027"
}
]
export const severites = [
{
label: "Unusable",
value: "10028"
},
{
label: "Gray screen",
value: "10030"
}
, {
label: "Data",
value: "10032"
},
{
label: "Malfunctioning",
value: "10033"
},
{
label: "Layout Issue",
value: "10034"
}
]