Skip to content

feat: husky #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,33 @@ jobs:

- name: Build
run: bunx tsc

commitlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Use Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install commitlint
run: |
bun add -d @commitlint/cli @commitlint/config-conventional

- name: Print versions
run: |
git --version
bun --version
bunx commitlint --version

- name: Validate current commit (last commit) with commitlint
if: github.event_name == 'push'
run: bunx commitlint --from HEAD~1 --to HEAD --verbose

- name: Validate PR commits with commitlint
if: github.event_name == 'pull_request'
run: bunx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
2 changes: 2 additions & 0 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bun run lint
bunx commitlint --edit $1
2 changes: 2 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bun run format
bunx tsc --noEmit
1 change: 1 addition & 0 deletions commitlint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { extends: ["@commitlint/config-conventional"] };
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"start": "bun ./dist/index.js",
"watch": "bunx tsc -w",
"format": "bunx prettier --write .",
"lint": "eslint . --fix "
"lint": "eslint . --fix ",
"prepare": "husky"
},
"keywords": [
"canvas",
Expand All @@ -33,6 +34,8 @@
"url": "^0.11.4"
},
"devDependencies": {
"@commitlint/cli": "^19.4.1",
"@commitlint/config-conventional": "^19.4.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.9.1",
"@types/eslint__js": "^8.42.3",
Expand All @@ -41,6 +44,7 @@
"@typescript-eslint/parser": "^8.3.0",
"eslint": "^9.9.1",
"globals": "^15.9.0",
"husky": "^9.1.5",
"supabase": "^1.191.3",
"typescript-eslint": "^8.3.0"
}
Expand Down
61 changes: 61 additions & 0 deletions src/commands/canvas/discussion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
ChatInputCommandInteraction,
Client,
EmbedBuilder,
SlashCommandBuilder,
} from "discord.js";
import { randomColor } from "../../helpers/colors";
import { Command, DiscussionTopic } from "../../types";
import { getDiscussions } from "../../helpers/api";
import { convert } from "html-to-text";
import { CourseSelector } from "../../components/dropdown/CourseSelector";

export const data = new SlashCommandBuilder()
.setName("discussion")
.setDescription("Fetches the latest discussion from Canvas")
.setDMPermission(false);

export async function execute(
interaction: ChatInputCommandInteraction,
client: Client,
) {
try {
const commandData: Command["data"] = {
name: "discussion",
permissions: [],
aliases: [],
};
const userId: string = interaction.user.id;
await interaction.deferReply({ ephemeral: true });

const courseId = await CourseSelector(interaction, userId);
if (!courseId) return;

const discussions: DiscussionTopic[] = await getDiscussions(
userId,
parseInt(courseId),
);
if (!discussions || discussions.length === 0) {
await interaction.editReply("No discussions found.");
return;
}

const discussion = discussions[0];
const embed = new EmbedBuilder()
.setColor(randomColor())
.setTitle(discussion.title)
.setURL(discussion.html_url)
.setDescription(convert(discussion.message))
.setTimestamp(new Date(discussion.posted_at))
.setFooter({ text: "Next Canvas check in 24 hours." });

await interaction.editReply({ embeds: [embed] });
return { data: commandData };
} catch (error) {
console.error(
`Error fetching discussion for user ${client.user?.id}:`,
error,
);
await interaction.editReply("There was an error fetching the discussion.");
}
}
5 changes: 2 additions & 3 deletions src/commands/canvas/missing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ export async function execute(interaction: ChatInputCommandInteraction) {
aliases: [],
};
const userId: string = interaction.user.id;
const userAssignments: MissingAssignmentResponse = await getAllAssignments(
userId,
);
const userAssignments: MissingAssignmentResponse =
await getAllAssignments(userId);

if (userAssignments.courses.length === 0) {
await interaction.reply({
Expand Down
80 changes: 80 additions & 0 deletions src/components/dropdown/CourseSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
ActionRowBuilder,
StringSelectMenuBuilder,
ComponentType,
ChatInputCommandInteraction,
} from "discord.js";
import { fetchCourses } from "../../helpers/api";
import { Course } from "../../types";

const SELECTION_TIMEOUT = 60000;

export async function CourseSelector(
interaction: ChatInputCommandInteraction,
userId: string,
): Promise<string | null> {
try {
const courses = await fetchCourses(userId);
if (courses.length === 0) {
await interaction.reply({
content: "You have no active courses.",
ephemeral: true,
});
return null;
}

const selectMenu = new StringSelectMenuBuilder()
.setCustomId("course_select")
.setPlaceholder("Select a course")
.addOptions(
courses.map((course: Course) => ({
label: course.name,
value: course.id.toString(),
})),
);

const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
selectMenu,
);

await interaction.reply({
content: "Please select a course:",
components: [row],
ephemeral: true,
});

return new Promise((resolve) => {
const collector = interaction.channel?.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
time: SELECTION_TIMEOUT,
});

collector?.on("collect", async (i) => {
const courseId = i.values[0];
await i.update({
content: `You selected course ID: ${courseId}`,
components: [],
});
resolve(courseId);
});

collector?.on("end", async (collected) => {
if (collected.size === 0) {
await interaction.followUp({
content: "Course selection timed out. Please try again.",
ephemeral: true,
});
resolve(null);
}
});
});
} catch (error) {
console.error(`Error fetching courses for user ${userId}:`, error);
await interaction.reply({
content:
"An error occurred while fetching your courses. Please try again later.",
ephemeral: true,
});
return null;
}
}
2 changes: 1 addition & 1 deletion src/events/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { postAssignment } from "./assignmentChecker";
const clientReadyEvent: BotEvent = {
name: Events.ClientReady,
once: true,
async execute(client: Client) {
execute: async (client: Client) => {
console.log(`Up! Logged In As ${client?.user?.tag}`);
client?.user?.setActivity("Your Assignments", {
type: ActivityType.Watching,
Expand Down
23 changes: 22 additions & 1 deletion src/helpers/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import axios, { AxiosRequestConfig } from "axios";
import { getCanvasID, getCanvasToken } from "./supabase";
import { AnnouncementPost, Course, Assignment } from "../types";
import {
AnnouncementPost,
Course,
Assignment,
DiscussionTopic,
} from "../types";
import { CONFIG } from "../config";

export async function fetchData<T>(
Expand Down Expand Up @@ -185,3 +190,19 @@ export async function fetchAssignmentChecker(
return [];
}
}

export async function getDiscussions(
userId: string,
courseId: number,
): Promise<DiscussionTopic[]> {
try {
const discussions = await fetchData<DiscussionTopic[]>(
userId,
`courses/${courseId}/discussion_topics`,
);
return discussions;
} catch (error) {
console.error(`Failed to fetch discussions for course ${courseId}:`, error);
throw new Error("Error fetching discussions.");
}
}
121 changes: 120 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface AnnouncementPost extends DataItem {
title?: string;
html_url?: string;
postLink?: string;
posted_at?: string;
posted_at?: Date;
}

export interface MissingAssignmentResponse {
Expand Down Expand Up @@ -117,3 +117,122 @@ export interface SelectedSchool {
export interface DataItem {
id: number | string;
}

export interface DiscussionPermissions {
attach: boolean;
update: boolean;
reply: boolean;
delete: boolean;
manage_assign_to: boolean;
require_initial_post: boolean | null;
user_can_see_posts: boolean;
}

export interface DiscussionAssignment {
id: number;
description: string;
due_at: Date;
unlock_at: Date;
lock_at: Date;
points_possible: number;
grading_type: string;
assignment_group_id: number;
grading_standard_id: number | null;
created_at: Date;
updated_at: Date;
peer_reviews: boolean;
automatic_peer_reviews: boolean;
position: number;
grade_group_students_individually: boolean;
anonymous_peer_reviews: boolean;
group_category_id: number | null;
post_to_sis: boolean;
moderated_grading: boolean;
omit_from_final_grade: boolean;
intra_group_peer_reviews: boolean;
anonymous_instructor_annotations: boolean;
anonymous_grading: boolean;
graders_anonymous_to_graders: boolean;
grader_count: number;
grader_comments_visible_to_graders: boolean;
final_grader_id: number | null;
grader_names_visible_to_final_grader: boolean;
allowed_attempts: number;
annotatable_attachment_id: number | null;
hide_in_gradebook: boolean;
secure_params: string;
lti_context_id: string;
course_id: number;
name: string;
submission_types: string[];
has_submitted_submissions: boolean;
due_date_required: boolean;
max_name_length: number;
in_closed_grading_period: boolean;
graded_submissions_exist: boolean;
is_quiz_assignment: boolean;
can_duplicate: boolean;
original_course_id: number | null;
original_assignment_id: number | null;
original_lti_resource_link_id: number | null;
original_assignment_name: string | null;
original_quiz_id: number | null;
workflow_state: string;
important_dates: boolean;
muted: boolean;
html_url: string;
published: boolean;
only_visible_to_overrides: boolean;
visible_to_everyone: boolean;
locked_for_user: boolean;
submissions_download_url: string;
post_manually: boolean;
anonymize_students: boolean;
require_lockdown_browser: boolean;
restrict_quantitative_data: boolean;
todo_date: string | null;
is_announcement: boolean;
}

export interface DiscussionTopic extends DataItem {
title: string;
last_reply_at: Date | null;
created_at: Date;
delayed_post_at: Date | null;
posted_at: Date;
assignment_id: number;
root_topic_id: number | null;
position: number | null;
podcast_has_student_posts: boolean;
discussion_type: string;
lock_at: Date | null;
allow_rating: boolean;
only_graders_can_rate: boolean;
sort_by_rating: boolean;
is_section_specific: boolean;
anonymous_state: string | null;
summary_enabled: boolean;
user_name: string | null;
discussion_subentry_count: number;
permissions: DiscussionPermissions;
read_state: string;
unread_count: number;
subscribed: boolean;
attachments: File[];
published: boolean;
can_unpublish: boolean;
locked: boolean;
can_lock: boolean;
comments_disabled: boolean;
author: string[];
html_url: string;
url: string;
pinned: boolean;
group_category_id: number | null;
can_group: boolean;
topic_children: string[];
group_topic_children: string[];
locked_for_user: boolean;
message: string;
assignment: DiscussionAssignment;
}
Loading