generated from nhcarrigan/template
### Explanation _No response_ ### Issue _No response_ ### Attestations - [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [x] I have pinned the dependencies to a specific patch version. ### Style - [x] I have run the linter and resolved any errors. - [x] My pull request uses an appropriate title, matching the conventional commit standards. - [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning Major - My pull request introduces a breaking change. Reviewed-on: #1 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("about").
|
||||
setDescription("Learn more about this bot!");
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
import { priorityChoices } from "../config/priorityChoices.js";
|
||||
import { statusChoices } from "../config/statusChoices.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("create").
|
||||
setDescription("Create a new task").
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("title").
|
||||
setDescription("The title for your task").
|
||||
setMaxLength(256).
|
||||
setRequired(true);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("description").
|
||||
setDescription("The description for your task").
|
||||
setMaxLength(2048).
|
||||
setRequired(true);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("status").
|
||||
setDescription("The status for your task").
|
||||
setChoices(statusChoices).
|
||||
setRequired(true);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("priority").
|
||||
setDescription("The priority for your task").
|
||||
setChoices(priorityChoices).
|
||||
setRequired(true);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("category").
|
||||
setDescription("The category for your task").
|
||||
setRequired(true).
|
||||
setMaxLength(1024);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("due-date").
|
||||
setDescription("The date this task is due in YYYY/MM/DD format").
|
||||
setRequired(false).
|
||||
setMinLength(10).
|
||||
setMaxLength(10);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("list").
|
||||
setDescription("List your currently active tasks.");
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("recategorise").
|
||||
setDescription("Update the category for a task.").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to update.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("category").
|
||||
setDescription("The new category for your task.").
|
||||
setRequired(true).
|
||||
setMaxLength(1024);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("redescribe").
|
||||
setDescription("Update the description for a task.").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to update.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("description").
|
||||
setDescription("The new description for your task").
|
||||
setMaxLength(2048).
|
||||
setRequired(true);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
import { priorityChoices } from "../config/priorityChoices.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("reprioritise").
|
||||
setDescription("Update the priority for a task.").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to update.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("priority").
|
||||
setDescription("The priority for your task").
|
||||
setChoices(priorityChoices).
|
||||
setRequired(true);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
import { statusChoices } from "../config/statusChoices.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("restate").
|
||||
setDescription("Update the status for a task.").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to update.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("status").
|
||||
setDescription("The status for your task").
|
||||
setChoices(statusChoices).
|
||||
setRequired(true);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("retarget").
|
||||
setDescription("Update the due date for a task.").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to update.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("due-date").
|
||||
setDescription("The date this task is due in YYYY/MM/DD format").
|
||||
setRequired(false).
|
||||
setMinLength(10).
|
||||
setMaxLength(10);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("retitle").
|
||||
setDescription("Update the title for a task.").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to update.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
}).
|
||||
addStringOption((option) => {
|
||||
return option.
|
||||
setName("title").
|
||||
setDescription("The new title for your task.").
|
||||
setRequired(true).
|
||||
setMaxLength(256);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import {
|
||||
ApplicationIntegrationType,
|
||||
SlashCommandBuilder,
|
||||
InteractionContextType,
|
||||
} from "discord.js";
|
||||
|
||||
const command = new SlashCommandBuilder().
|
||||
setContexts(
|
||||
InteractionContextType.BotDM,
|
||||
InteractionContextType.Guild,
|
||||
InteractionContextType.PrivateChannel,
|
||||
).
|
||||
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
|
||||
setName("view").
|
||||
setDescription("View a task").
|
||||
addIntegerOption((option) => {
|
||||
return option.
|
||||
setName("number").
|
||||
setDescription("The number of the task you wish to view.").
|
||||
setRequired(true).
|
||||
setMinValue(1);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production.
|
||||
console.log(JSON.stringify(command.toJSON()));
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import type { Priority } from "../interfaces/priority.js";
|
||||
import type { APIApplicationCommandOptionChoice } from "discord.js";
|
||||
|
||||
export const priorityChoices: Array<APIApplicationCommandOptionChoice<Priority>>
|
||||
= [
|
||||
{ name: "None", value: "none" },
|
||||
{ name: "Low", value: "low" },
|
||||
{ name: "Medium", value: "medium" },
|
||||
{ name: "High", value: "high" },
|
||||
{ name: "Critical", value: "critical" },
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import type { Priority } from "../interfaces/priority.js";
|
||||
|
||||
export const priorityNames: Record<Priority, string> = {
|
||||
critical: "Critical",
|
||||
high: "High",
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
none: "None",
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import type { Status } from "../interfaces/status.js";
|
||||
import type { APIApplicationCommandOptionChoice } from "discord.js";
|
||||
|
||||
export const statusChoices: Array<APIApplicationCommandOptionChoice<Status>>
|
||||
= [
|
||||
{ name: "TODO", value: "todo" },
|
||||
{ name: "In Progress", value: "in-progress" },
|
||||
{ name: "In Review", value: "in-review" },
|
||||
{ name: "Complete", value: "complete" },
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- These need to follow the requirements for Discord choices. */
|
||||
import type { Status } from "../interfaces/status.js";
|
||||
|
||||
export const statusNames: Record<Status, string> = {
|
||||
"complete": "Complete",
|
||||
"in-progress": "In Progress",
|
||||
"in-review": "In Review",
|
||||
"todo": "To Do",
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
export const database = new PrismaClient();
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { Client, Events, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "./db/database.js";
|
||||
import { about } from "./modules/about.js";
|
||||
import { create } from "./modules/create.js";
|
||||
import { list } from "./modules/list.js";
|
||||
import { recategorise } from "./modules/recategorise.js";
|
||||
import { redescribe } from "./modules/redescribe.js";
|
||||
import { restate } from "./modules/restate.js";
|
||||
import { retarget } from "./modules/retarget.js";
|
||||
import { retitle } from "./modules/retitle.js";
|
||||
import { view } from "./modules/view.js";
|
||||
import { instantiateServer } from "./server/serve.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
|
||||
const commands: Record<
|
||||
string,
|
||||
(interaction: ChatInputCommandInteraction)=> Promise<void>
|
||||
> = {
|
||||
about,
|
||||
create,
|
||||
list,
|
||||
recategorise,
|
||||
redescribe,
|
||||
restate,
|
||||
retarget,
|
||||
retitle,
|
||||
view,
|
||||
};
|
||||
|
||||
process.on("unhandledRejection", (error) => {
|
||||
if (error instanceof Error) {
|
||||
void logger.error("Unhandled Rejection", error);
|
||||
return;
|
||||
}
|
||||
void logger.error("unhandled rejection", new Error(String(error)));
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
if (error instanceof Error) {
|
||||
void logger.error("Uncaught Exception", error);
|
||||
return;
|
||||
}
|
||||
void logger.error("uncaught exception", new Error(String(error)));
|
||||
});
|
||||
|
||||
const client = new Client({
|
||||
intents: [],
|
||||
});
|
||||
|
||||
client.on(Events.InteractionCreate, (interaction) => {
|
||||
if (interaction.isChatInputCommand()) {
|
||||
const handler = commands[interaction.commandName];
|
||||
if (handler) {
|
||||
void handler(interaction);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.on(Events.EntitlementCreate, (entitlement) => {
|
||||
void logger.log("info", `User ${entitlement.userId} has subscribed!`);
|
||||
});
|
||||
|
||||
client.on(Events.EntitlementDelete, (entitlement) => {
|
||||
void logger.log("info", `User ${entitlement.userId} has unsubscribed... :c`);
|
||||
});
|
||||
|
||||
client.on(Events.ClientReady, () => {
|
||||
void logger.log("debug", "Bot is ready.");
|
||||
});
|
||||
|
||||
instantiateServer();
|
||||
await client.login(process.env.DISCORD_TOKEN);
|
||||
await database.$connect();
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
export type Priority = "none" | "low" | "medium" | "high" | "critical";
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
export type Status = "todo" | "in-progress" | "in-review" | "complete";
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
EmbedBuilder,
|
||||
MessageFlags,
|
||||
type ChatInputCommandInteraction,
|
||||
} from "discord.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Responds with information about the bot.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function -- Refactor at a later time.
|
||||
export const about = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
|
||||
const version = process.env.npm_package_version ?? "Unknown";
|
||||
const commit = execSync("git rev-parse --short HEAD").toString().
|
||||
trim();
|
||||
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTitle("About Melody Iuvo");
|
||||
embed.setDescription(
|
||||
// eslint-disable-next-line stylistic/max-len -- It's a long string.
|
||||
"Melody Iuvo is a Discord bot that allows you to track your tasks, deadlines, plans, and other goals. To use the bot, type `/` and select one of her commands!",
|
||||
);
|
||||
embed.addFields(
|
||||
{
|
||||
name: "Running Version",
|
||||
value: version,
|
||||
},
|
||||
{
|
||||
name: "Current Commit",
|
||||
value: commit,
|
||||
},
|
||||
);
|
||||
|
||||
const supportButton = new ButtonBuilder().
|
||||
setLabel("Need help?").
|
||||
setStyle(ButtonStyle.Link).
|
||||
setURL("https://chat.nhcarrigan.com");
|
||||
const sourceButton = new ButtonBuilder().
|
||||
setLabel("Source Code").
|
||||
setStyle(ButtonStyle.Link).
|
||||
setURL("https://git.nhcarrigan.com/nhcarrigan/melody-iuvo");
|
||||
const subscribeButton = new ButtonBuilder().
|
||||
setStyle(ButtonStyle.Premium).
|
||||
setSKUId("1338672773261951026");
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
supportButton,
|
||||
sourceButton,
|
||||
subscribeButton,
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
components: [ row ],
|
||||
embeds: [ embed ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("about command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Accepts the necessary properties to create a new task for the
|
||||
* user in the database.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const create = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = interaction.options.getString("title", true);
|
||||
const description = interaction.options.getString("description", true);
|
||||
const dueDate = interaction.options.getString("due-date", false);
|
||||
const dueAt = new Date(dueDate ?? Date.now());
|
||||
const status = interaction.options.getString("status", true);
|
||||
const category = interaction.options.getString("category", true);
|
||||
const priority = interaction.options.getString("priority", true);
|
||||
|
||||
const currentNumber = await database.tasks.count({
|
||||
where: { userId: interaction.user.id },
|
||||
});
|
||||
const number = currentNumber + 1;
|
||||
const userId = interaction.user.id;
|
||||
|
||||
const task = await database.tasks.create({
|
||||
data: {
|
||||
category,
|
||||
description,
|
||||
dueAt,
|
||||
number,
|
||||
priority,
|
||||
status,
|
||||
title,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
await interaction.editReply({
|
||||
content: `Task #${task.number.toString()} created: ${task.title}`,
|
||||
embeds: [ generateTaskEmbed(task) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("create command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Fetches all non-complete tasks for the user and sends them in a list.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const list = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tasks = await database.tasks.findMany({
|
||||
where: {
|
||||
status: {
|
||||
not: "complete",
|
||||
},
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
});
|
||||
if (tasks.length === 0) {
|
||||
await interaction.editReply({
|
||||
content: "You have no upcoming tasks. Great work!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
tasks.sort((a, b) => {
|
||||
return a.dueAt.getTime() - b.dueAt.getTime();
|
||||
});
|
||||
|
||||
const firstTen = tasks.slice(0, 10);
|
||||
|
||||
await interaction.editReply({
|
||||
content: "Here are your top upcoming tasks:",
|
||||
embeds: firstTen.map((task) => {
|
||||
return generateTaskEmbed(task);
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("list command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Updates the category of a task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const recategorise = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
const category = interaction.options.getString("category", true);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await database.tasks.update({
|
||||
data: {
|
||||
category,
|
||||
},
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: `Recategorised task #${updated.number.toString()}:`,
|
||||
embeds: [ generateTaskEmbed(updated) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("recategorise command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Updates the description of a task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const redescribe = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
const description = interaction.options.getString("description", true);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await database.tasks.update({
|
||||
data: {
|
||||
description,
|
||||
},
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: `Redescribed task #${updated.number.toString()}:`,
|
||||
embeds: [ generateTaskEmbed(updated) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("redescribe command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Updates the priority of a task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const restate = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
const priority = interaction.options.getString("priority", true);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await database.tasks.update({
|
||||
data: {
|
||||
priority,
|
||||
},
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: `Restated task #${updated.number.toString()}:`,
|
||||
embeds: [ generateTaskEmbed(updated) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("restate command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Updates the status of a task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const restate = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
const status = interaction.options.getString("status", true);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await database.tasks.update({
|
||||
data: {
|
||||
status,
|
||||
},
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: `Restated task #${updated.number.toString()}:`,
|
||||
embeds: [ generateTaskEmbed(updated) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("restate command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Updates the due date of a task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const retarget = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
const dueDate = interaction.options.getString("dueDate", true);
|
||||
const dueAt = new Date(dueDate);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await database.tasks.update({
|
||||
data: {
|
||||
dueAt,
|
||||
},
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: `Retargeted task #${updated.number.toString()}:`,
|
||||
embeds: [ generateTaskEmbed(updated) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("retarget command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Updates the description of a task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const retitle = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
const title = interaction.options.getString("title", true);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await database.tasks.update({
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await interaction.editReply({
|
||||
content: `Retitled task #${updated.number.toString()}:`,
|
||||
embeds: [ generateTaskEmbed(updated) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("retitle command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
import { database } from "../db/database.js";
|
||||
import { generateTaskEmbed } from "../utils/generateTaskEmbed.js";
|
||||
import { isSubscribed } from "../utils/isSubscribed.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { replyToError } from "../utils/replyToError.js";
|
||||
|
||||
/**
|
||||
* Generates an embed for a given task.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const view = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] });
|
||||
const sub = await isSubscribed(interaction);
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const number = interaction.options.getInteger("number", true);
|
||||
|
||||
const task = await database.tasks.findUnique({
|
||||
where: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Index mapping.
|
||||
userId_number: {
|
||||
number: number,
|
||||
userId: interaction.user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (task === null) {
|
||||
await interaction.editReply({ content: `You do not have a task with the number ${number.toString()}.` });
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [ generateTaskEmbed(task) ],
|
||||
});
|
||||
} catch (error) {
|
||||
await replyToError(interaction);
|
||||
if (error instanceof Error) {
|
||||
await logger.error("view command", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import fastify from "fastify";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Melody Iuvo</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="A task management bot for Discord!" />
|
||||
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Melody Iuvo</h1>
|
||||
<section>
|
||||
<p>A task management bot for Discord!</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Links</h2>
|
||||
<p>
|
||||
<a href="https://git.nhcarrigan.com/nhcarrigan/melody-iuvo">
|
||||
<i class="fa-solid fa-code"></i> Source Code
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://docs.nhcarrigan.com/">
|
||||
<i class="fa-solid fa-book"></i> Documentation
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://chat.nhcarrigan.com">
|
||||
<i class="fa-solid fa-circle-info"></i> Support
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
/**
|
||||
* Starts up a web server for health monitoring.
|
||||
*/
|
||||
export const instantiateServer = (): void => {
|
||||
try {
|
||||
const server = fastify({
|
||||
logger: false,
|
||||
});
|
||||
|
||||
server.get("/", (_request, response) => {
|
||||
response.header("Content-Type", "text/html");
|
||||
response.send(html);
|
||||
});
|
||||
|
||||
server.listen({ port: 5443 }, (error) => {
|
||||
if (error) {
|
||||
void logger.error("instantiate server", error);
|
||||
return;
|
||||
}
|
||||
void logger.log("debug", "Server listening on port 5443.");
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
void logger.error("instantiate server", error);
|
||||
return;
|
||||
}
|
||||
void logger.error("instantiate server", new Error("Unknown error"));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import { EmbedBuilder, type ColorResolvable } from "discord.js";
|
||||
import { priorityNames } from "../config/priorityNames.js";
|
||||
import { statusNames } from "../config/statusNames.js";
|
||||
import type { Priority } from "../interfaces/priority.js";
|
||||
import type { Status } from "../interfaces/status.js";
|
||||
import type { Tasks } from "@prisma/client";
|
||||
|
||||
const colours: Record<Status, ColorResolvable> = {
|
||||
"complete": 0x00_FF_00,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- matching the status names.
|
||||
"in-progress": 0xFF_77_00,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- matching the status names.
|
||||
"in-review": 0xFF_FF_00,
|
||||
"todo": 0xFF_00_00,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a Discord embed for a task.
|
||||
* @param task -- The task record from the database.
|
||||
* @returns An EmbedBuilder.
|
||||
*/
|
||||
export const generateTaskEmbed = (task: Tasks): EmbedBuilder => {
|
||||
const embed = new EmbedBuilder();
|
||||
embed.setTitle(task.title);
|
||||
embed.setDescription(task.description);
|
||||
embed.addFields(
|
||||
{
|
||||
inline: true,
|
||||
name: "Category",
|
||||
value: task.category,
|
||||
},
|
||||
{
|
||||
inline: true,
|
||||
name: "Status",
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Totally being lazy here.
|
||||
value: statusNames[task.status as Status],
|
||||
},
|
||||
{
|
||||
inline: true,
|
||||
name: "Priority",
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Totally being lazy here.
|
||||
value: priorityNames[task.priority as Priority],
|
||||
},
|
||||
{
|
||||
inline: true,
|
||||
name: "Due Date",
|
||||
value: task.dueAt.toLocaleDateString("en-GB"),
|
||||
},
|
||||
{
|
||||
inline: true,
|
||||
name: "Created At",
|
||||
value: task.createdAt.toLocaleDateString("en-GB"),
|
||||
},
|
||||
{
|
||||
inline: true,
|
||||
name: "Updated At",
|
||||
value: task.updatedAt.toLocaleDateString("en-GB"),
|
||||
},
|
||||
);
|
||||
embed.setFooter({
|
||||
text: `Task #${task.number.toString()}`,
|
||||
});
|
||||
embed.setColor(task.status in colours
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We narrowed it smh.
|
||||
? colours[task.status as Status]
|
||||
: 0x00_00_00);
|
||||
return embed;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
type ChatInputCommandInteraction,
|
||||
} from "discord.js";
|
||||
|
||||
/**
|
||||
* Checks if a user has an active entitlement (subscription) for the bot.
|
||||
* If they do not, it responds to the interaction with a button to subscribe.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
* @returns A boolean indicating whether the user is subscribed.
|
||||
*/
|
||||
export const isSubscribed = async(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): Promise<boolean> => {
|
||||
const isEntitled = interaction.entitlements.find((entitlement) => {
|
||||
return entitlement.userId === interaction.user.id && entitlement.isActive();
|
||||
});
|
||||
|
||||
if (!isEntitled && interaction.user.id !== "465650873650118659") {
|
||||
const subscribeButton = new ButtonBuilder().
|
||||
setStyle(ButtonStyle.Premium).
|
||||
setSKUId("1338755540397985873");
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
subscribeButton,
|
||||
);
|
||||
await interaction.editReply({
|
||||
components: [ row ],
|
||||
content: "You must be subscribed to use this feature.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Logger } from "@nhcarrigan/logger";
|
||||
|
||||
export const logger = new Logger(
|
||||
"Melody Iuvo",
|
||||
process.env.LOG_TOKEN ?? "",
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
type ChatInputCommandInteraction,
|
||||
type MessageContextMenuCommandInteraction,
|
||||
} from "discord.js";
|
||||
|
||||
/**
|
||||
* Responds to an interaction with a generic error message.
|
||||
* @param interaction -- The interaction payload from Discord.
|
||||
*/
|
||||
export const replyToError = async(
|
||||
interaction:
|
||||
| ChatInputCommandInteraction
|
||||
| MessageContextMenuCommandInteraction,
|
||||
): Promise<void> => {
|
||||
const button = new ButtonBuilder().setLabel("Need help?").
|
||||
setStyle(ButtonStyle.Link).
|
||||
setURL("https://chat.nhcarrigan.com");
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({
|
||||
components: [ row ],
|
||||
content: "An error occurred while running this command.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.reply({
|
||||
components: [ row ],
|
||||
content: "An error occurred while running this command.",
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user