generated from nhcarrigan/template
feat: initial prototype (#1)
Some checks failed
Node.js CI / Lint and Test (push) Has been cancelled
Some checks failed
Node.js CI / Lint and Test (push) Has been cancelled
### 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 is contained in:
parent
08f232f0ee
commit
f6e32e0e68
38
.gitea/workflows/ci.yml
Normal file
38
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
name: Node.js CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint and Test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Source Files
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js v22
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Lint Source Files
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Verify Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: pnpm run test
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
prod
|
||||||
|
coverage
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.validate": ["typescript"]
|
||||||
|
}
|
18
README.md
18
README.md
@ -1,20 +1,10 @@
|
|||||||
# New Repository Template
|
# Melody Iuvo
|
||||||
|
|
||||||
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository.
|
Melody is a powerful task management bot for Discord.
|
||||||
|
|
||||||
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
|
|
||||||
|
|
||||||
## Readme
|
|
||||||
|
|
||||||
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
|
|
||||||
|
|
||||||
<!-- # Project Name
|
|
||||||
|
|
||||||
Project Description
|
|
||||||
|
|
||||||
## Live Version
|
## Live Version
|
||||||
|
|
||||||
This page is currently deployed. [View the live website.]
|
[Add her to your account](https://discord.com/oauth2/authorize?client_id=1338753576583041074)!
|
||||||
|
|
||||||
## Feedback and Bugs
|
## Feedback and Bugs
|
||||||
|
|
||||||
@ -36,4 +26,4 @@ Copyright held by Naomi Carrigan.
|
|||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. -->
|
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.
|
||||||
|
5
eslint.config.js
Normal file
5
eslint.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import NaomisConfig from "@nhcarrigan/eslint-config";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...NaomisConfig
|
||||||
|
];
|
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "cordelia-taryne",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "An AI-powered multi-purpose assistant for Discord.",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf prod && tsc",
|
||||||
|
"lint": "eslint src --max-warnings 0",
|
||||||
|
"start": "op run --env-file=prod.env --no-masking -- node prod/index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 0"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Naomi Carrigan",
|
||||||
|
"license": "See license in LICENSE.md",
|
||||||
|
"devDependencies": {
|
||||||
|
"@nhcarrigan/eslint-config": "5.1.0",
|
||||||
|
"@nhcarrigan/typescript-config": "4.0.0",
|
||||||
|
"@types/node": "22.13.1",
|
||||||
|
"eslint": "9.20.0",
|
||||||
|
"prisma": "6.3.1",
|
||||||
|
"typescript": "5.7.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "0.36.3",
|
||||||
|
"@nhcarrigan/logger": "1.0.0",
|
||||||
|
"@prisma/client": "6.3.1",
|
||||||
|
"discord.js": "14.18.0",
|
||||||
|
"fastify": "5.2.1"
|
||||||
|
}
|
||||||
|
}
|
4729
pnpm-lock.yaml
generated
Normal file
4729
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
prisma/schema.prisma
Normal file
25
prisma/schema.prisma
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "mongodb"
|
||||||
|
url = env("MONGO_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tasks {
|
||||||
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
|
userId String
|
||||||
|
number Int
|
||||||
|
title String
|
||||||
|
category String
|
||||||
|
description String
|
||||||
|
status String
|
||||||
|
priority String
|
||||||
|
dueAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([userId, number], map: "userId_number")
|
||||||
|
@@index([userId], map: "userId")
|
||||||
|
}
|
3
prod.env
Normal file
3
prod.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
DISCORD_TOKEN="op://Environment Variables - Naomi/Melody Iuvo/discord_token"
|
||||||
|
MONGO_URL="op://Environment Variables - Naomi/Melody Iuvo/mongo_url"
|
||||||
|
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
|
24
src/commands/about.ts
Normal file
24
src/commands/about.ts
Normal file
@ -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()));
|
69
src/commands/create.ts
Normal file
69
src/commands/create.ts
Normal file
@ -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()));
|
24
src/commands/list.ts
Normal file
24
src/commands/list.ts
Normal file
@ -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()));
|
38
src/commands/recategorise.ts
Normal file
38
src/commands/recategorise.ts
Normal file
@ -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()));
|
38
src/commands/redescribe.ts
Normal file
38
src/commands/redescribe.ts
Normal file
@ -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()));
|
39
src/commands/reprioritise.ts
Normal file
39
src/commands/reprioritise.ts
Normal file
@ -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()));
|
39
src/commands/restate.ts
Normal file
39
src/commands/restate.ts
Normal file
@ -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()));
|
39
src/commands/retarget.ts
Normal file
39
src/commands/retarget.ts
Normal file
@ -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()));
|
38
src/commands/retitle.ts
Normal file
38
src/commands/retitle.ts
Normal file
@ -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()));
|
31
src/commands/view.ts
Normal file
31
src/commands/view.ts
Normal file
@ -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()));
|
17
src/config/priorityChoices.ts
Normal file
17
src/config/priorityChoices.ts
Normal file
@ -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" },
|
||||||
|
];
|
15
src/config/priorityNames.ts
Normal file
15
src/config/priorityNames.ts
Normal file
@ -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",
|
||||||
|
};
|
16
src/config/statusChoices.ts
Normal file
16
src/config/statusChoices.ts
Normal file
@ -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" },
|
||||||
|
];
|
15
src/config/statusNames.ts
Normal file
15
src/config/statusNames.ts
Normal file
@ -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",
|
||||||
|
};
|
9
src/db/database.ts
Normal file
9
src/db/database.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @copyright nhcarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
export const database = new PrismaClient();
|
78
src/index.ts
Normal file
78
src/index.ts
Normal file
@ -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();
|
7
src/interfaces/priority.ts
Normal file
7
src/interfaces/priority.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @copyright nhcarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Priority = "none" | "low" | "medium" | "high" | "critical";
|
7
src/interfaces/status.ts
Normal file
7
src/interfaces/status.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @copyright nhcarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Status = "todo" | "in-progress" | "in-review" | "complete";
|
78
src/modules/about.ts
Normal file
78
src/modules/about.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
64
src/modules/create.ts
Normal file
64
src/modules/create.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
59
src/modules/list.ts
Normal file
59
src/modules/list.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
68
src/modules/recategorise.ts
Normal file
68
src/modules/recategorise.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
68
src/modules/redescribe.ts
Normal file
68
src/modules/redescribe.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
68
src/modules/reprioritise.ts
Normal file
68
src/modules/reprioritise.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
68
src/modules/restate.ts
Normal file
68
src/modules/restate.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
69
src/modules/retarget.ts
Normal file
69
src/modules/retarget.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
68
src/modules/retitle.ts
Normal file
68
src/modules/retitle.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
53
src/modules/view.ts
Normal file
53
src/modules/view.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
75
src/server/serve.ts
Normal file
75
src/server/serve.ts
Normal file
@ -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"));
|
||||||
|
}
|
||||||
|
};
|
73
src/utils/generateTaskEmbed.ts
Normal file
73
src/utils/generateTaskEmbed.ts
Normal file
@ -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;
|
||||||
|
};
|
40
src/utils/isSubscribed.ts
Normal file
40
src/utils/isSubscribed.ts
Normal file
@ -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;
|
||||||
|
};
|
12
src/utils/logger.ts
Normal file
12
src/utils/logger.ts
Normal file
@ -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 ?? "",
|
||||||
|
);
|
38
src/utils/replyToError.ts
Normal file
38
src/utils/replyToError.ts
Normal file
@ -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.",
|
||||||
|
});
|
||||||
|
};
|
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@nhcarrigan/typescript-config",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./prod"
|
||||||
|
},
|
||||||
|
"exclude": ["test/**/*.ts", "vitest.config.ts"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user