Skip to content

Commit 674c4b4

Browse files
authored
Merge pull request #51 from ICEPrey/discussion-command
feat: discussion cmd
2 parents 56f3b82 + 903f04d commit 674c4b4

File tree

13 files changed

+356
-38
lines changed

13 files changed

+356
-38
lines changed

.github/dependabot.yml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
# To get started with Dependabot version updates, you'll need to specify which
2-
# package ecosystems to update and where the package manifests are located.
3-
# Please see the documentation for all configuration options:
4-
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5-
61
version: 2
72
updates:
8-
- package-ecosystem: "npm" # See documentation for possible values
9-
directory: "/" # Location of package manifests
3+
- package-ecosystem: "npm"
4+
directory: "/"
105
schedule:
116
interval: "weekly"
7+
open-pull-requests-limit: 10
8+
versioning-strategy: auto
9+
allow:
10+
- dependency-type: "direct"
11+
commit-message:
12+
prefix: "chore"
13+
include: "scope"
14+
labels:
15+
- "dependencies"
16+
ignore:
17+
- dependency-name: "*"
18+
update-types: ["version-update:semver-patch"]

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,33 @@ jobs:
3131

3232
- name: Build
3333
run: bunx tsc
34+
35+
commitlint:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Use Bun
43+
uses: oven-sh/setup-bun@v2
44+
with:
45+
bun-version: latest
46+
47+
- name: Install commitlint
48+
run: |
49+
bun add -d @commitlint/cli @commitlint/config-conventional
50+
51+
- name: Print versions
52+
run: |
53+
git --version
54+
bun --version
55+
bunx commitlint --version
56+
57+
- name: Validate current commit (last commit) with commitlint
58+
if: github.event_name == 'push'
59+
run: bunx commitlint --from HEAD~1 --to HEAD --verbose
60+
61+
- name: Validate PR commits with commitlint
62+
if: github.event_name == 'pull_request'
63+
run: bunx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

.husky/commit-msg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bun run lint
2+
bunx commitlint --edit $1

.husky/pre-commit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bun run format
2+
bunx tsc --noEmit

commitlint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default { extends: ["@commitlint/config-conventional"] };

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"start": "bun ./dist/index.js",
1010
"watch": "bunx tsc -w",
1111
"format": "bunx prettier --write .",
12-
"lint": "eslint . --fix "
12+
"lint": "eslint . --fix --ignore-pattern 'dist/*'",
13+
"prepare": "husky"
1314
},
1415
"keywords": [
1516
"canvas",
@@ -33,6 +34,8 @@
3334
"url": "^0.11.4"
3435
},
3536
"devDependencies": {
37+
"@commitlint/cli": "^19.4.1",
38+
"@commitlint/config-conventional": "^19.4.1",
3639
"@eslint/eslintrc": "^3.1.0",
3740
"@eslint/js": "^9.9.1",
3841
"@types/eslint__js": "^8.42.3",
@@ -41,6 +44,7 @@
4144
"@typescript-eslint/parser": "^8.3.0",
4245
"eslint": "^9.9.1",
4346
"globals": "^15.9.0",
47+
"husky": "^9.1.5",
4448
"supabase": "^1.191.3",
4549
"typescript-eslint": "^8.3.0"
4650
}

src/commands/canvas/assignments.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ChatInputCommandInteraction,
99
} from "discord.js";
1010
import { randomColor } from "../../helpers/colors";
11-
import { fetchAssignments, fetchCourses } from "../../helpers/api";
11+
import { fetchAssignments, getCourses } from "../../helpers/api";
1212
import { Course } from "../../types";
1313

1414
export const data = new SlashCommandBuilder()
@@ -17,7 +17,7 @@ export const data = new SlashCommandBuilder()
1717

1818
export async function execute(interaction: ChatInputCommandInteraction) {
1919
const userId = interaction.user.id;
20-
const courses = await fetchCourses(userId);
20+
const courses = await getCourses(userId);
2121
if (courses.length === 0) {
2222
await interaction.reply({
2323
content: "You have no courses.",

src/commands/canvas/discussion.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
ChatInputCommandInteraction,
3+
EmbedBuilder,
4+
SlashCommandBuilder,
5+
} from "discord.js";
6+
import { randomColor } from "../../helpers/colors";
7+
import { getDiscussions } from "../../helpers/api";
8+
import { convert } from "html-to-text";
9+
import { CourseSelector } from "../../components/dropdown/CourseSelector";
10+
11+
export const data = new SlashCommandBuilder()
12+
.setName("discussion")
13+
.setDescription("Fetches the latest discussion from Canvas")
14+
.setDMPermission(false);
15+
16+
export async function execute(interaction: ChatInputCommandInteraction) {
17+
try {
18+
const userId: string = interaction.user.id;
19+
await interaction.deferReply({ ephemeral: true });
20+
const courseId = await CourseSelector(interaction, userId);
21+
22+
if (!courseId) {
23+
await interaction.editReply("No course was selected. Command cancelled.");
24+
return;
25+
}
26+
27+
// Fetch discussions for the selected course
28+
const discussions = await getDiscussions(userId, parseInt(courseId));
29+
30+
if (!discussions || discussions.length === 0) {
31+
await interaction.editReply(
32+
"No discussions found for the selected course.",
33+
);
34+
return;
35+
}
36+
37+
const discussion = discussions[0];
38+
const embed = new EmbedBuilder()
39+
.setColor(randomColor())
40+
.setTitle(discussion.title)
41+
.setURL(discussion.html_url)
42+
.setDescription(convert(discussion.message))
43+
.setTimestamp(new Date(discussion.posted_at))
44+
.setFooter({ text: "Next Canvas check in 24 hours." });
45+
46+
await interaction.editReply({
47+
content: "Here's the latest discussion:",
48+
embeds: [embed],
49+
});
50+
} catch (error) {
51+
console.error(
52+
`Unexpected error in discussion command for user ${interaction.user.id}:`,
53+
error,
54+
);
55+
await interaction.editReply({
56+
content:
57+
"An unexpected error occurred. Please try again later or contact support if the issue persists.",
58+
});
59+
}
60+
}

src/commands/canvas/missing.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ export async function execute(interaction: ChatInputCommandInteraction) {
2020
aliases: [],
2121
};
2222
const userId: string = interaction.user.id;
23-
const userAssignments: MissingAssignmentResponse = await getAllAssignments(
24-
userId,
25-
);
23+
const userAssignments: MissingAssignmentResponse =
24+
await getAllAssignments(userId);
2625

2726
if (userAssignments.courses.length === 0) {
2827
await interaction.reply({
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
ActionRowBuilder,
3+
ChatInputCommandInteraction,
4+
Collection,
5+
ComponentType,
6+
StringSelectMenuBuilder,
7+
StringSelectMenuInteraction,
8+
} from "discord.js";
9+
import { getCourses } from "../../helpers/api";
10+
import { Course } from "../../types";
11+
const SELECTION_TIMEOUT = 60000;
12+
13+
export async function CourseSelector(
14+
interaction: ChatInputCommandInteraction,
15+
userId: string,
16+
): Promise<string | null> {
17+
try {
18+
const courses = await getCourses(userId);
19+
if (courses.length === 0) {
20+
await interaction.followUp({
21+
content: "You have no active courses.",
22+
ephemeral: true,
23+
});
24+
return null;
25+
}
26+
27+
const selectMenu = new StringSelectMenuBuilder()
28+
.setCustomId("course_select")
29+
.setPlaceholder("Select a course")
30+
.addOptions(
31+
courses.map((course: Course) => ({
32+
label: course.name,
33+
value: course.id.toString(),
34+
})),
35+
);
36+
37+
const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
38+
selectMenu,
39+
);
40+
const followUpMessage = await interaction.followUp({
41+
content: "Please select a course:",
42+
components: [row],
43+
ephemeral: true,
44+
fetchReply: true,
45+
});
46+
47+
return new Promise<string | null>((resolve) => {
48+
const collector = followUpMessage.createMessageComponentCollector({
49+
componentType: ComponentType.StringSelect,
50+
time: SELECTION_TIMEOUT,
51+
});
52+
53+
collector.on("collect", async (i: StringSelectMenuInteraction) => {
54+
const courseId = i.values[0];
55+
await i.update({
56+
content: `You selected course ID: ${courseId}`,
57+
components: [],
58+
});
59+
collector.stop();
60+
resolve(courseId);
61+
});
62+
63+
collector.on(
64+
"end",
65+
async (collected: Collection<string, StringSelectMenuInteraction>) => {
66+
if (collected.size === 0) {
67+
await interaction.followUp({
68+
content: "Course selection timed out. Please try again.",
69+
ephemeral: true,
70+
});
71+
resolve(null);
72+
}
73+
},
74+
);
75+
});
76+
} catch (error) {
77+
console.error(`Error in CourseSelector for user ${userId}:`, error);
78+
await interaction.followUp({
79+
content:
80+
"An error occurred while fetching courses. Please try again later.",
81+
ephemeral: true,
82+
});
83+
return null;
84+
}
85+
}

0 commit comments

Comments
 (0)