mirror of
https://github.com/DarthKilroy/Spot.git
synced 2025-12-19 18:26:48 +00:00
Add create jira bug
This commit is contained in:
parent
6c8cf194a7
commit
1a7822ea69
13 changed files with 418 additions and 97 deletions
26
bind-commands.ts
Normal file
26
bind-commands.ts
Normal 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
102
bot.ts
|
|
@ -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
134
commands/jira/create-bug.ts
Normal 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
12
commands/utility/ping.ts
Normal 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
44
deploy-commands.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
12
events/guild-member-add.ts
Normal file
12
events/guild-member-add.ts
Normal 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);
|
||||
}
|
||||
32
events/interaction-create.ts
Normal file
32
events/interaction-create.ts
Normal 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
70
events/message-create.ts
Normal 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
3
events/ready.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const onReady = () => {
|
||||
console.log("Bot ready!")
|
||||
}
|
||||
|
|
@ -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);
|
||||
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 []
|
||||
}
|
||||
|
||||
} while (jsonResponse!.issues.length > 99 && jsonResponse!.total > 100);
|
||||
|
||||
return issues;
|
||||
|
|
|
|||
|
|
@ -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
13
utils/bind-events.ts
Normal 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
46
utils/jira.ts
Normal 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"
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue