diff --git a/package.json b/package.json index de520dfb..e0f44617 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@sapphire/ts-config": "^4.0.0", "@types/eventsource": "^1.1.11", "@types/express": "^4.17.17", + "@types/lodash": "^4.14.194", "@types/node": "^18.16.0", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", @@ -60,6 +61,7 @@ "dotenv": "^16.0.3", "eventsource": "^2.0.2", "express": "^4.18.2", + "lodash": "^4.17.21", "zod": "^3.21.4" } } diff --git a/src/database/structures/Guild.ts b/src/database/structures/Guild.ts index e8415fd5..645498be 100644 --- a/src/database/structures/Guild.ts +++ b/src/database/structures/Guild.ts @@ -3,6 +3,7 @@ import type GitCordClient from "#discord/lib/GitCordClient.js"; import { ChannelType, Collection, ForumChannel, Guild, TextChannel } from "discord.js"; import GitCordGuildWebhook from "./GuildWebhook.js"; import { randomBytes } from "node:crypto"; +import { GITHUB_AVATAR_URL } from "#shared/constants.js"; export default class GitCordGuild { public guild!: Guild; @@ -37,7 +38,7 @@ export default class GitCordGuild { */ public async create(channel: ForumChannel | TextChannel) { const type = channel.type === ChannelType.GuildForum ? "FORUM" : "CHANNEL"; - const webhook = await channel.createWebhook({ name: "GitCord", avatar: "https://cdn.ijskoud.dev/files/2zVGPBN3ZmId.webp" }).catch(() => { + const webhook = await channel.createWebhook({ name: "GitCord", avatar: GITHUB_AVATAR_URL }).catch(() => { throw new Error("Unable to create a webhook, probably missing permissions."); }); diff --git a/src/github/embeds/Ref/CreateRefEmbed.ts b/src/github/embeds/Ref/CreateRefEmbed.ts new file mode 100644 index 00000000..a4d84f2b --- /dev/null +++ b/src/github/embeds/Ref/CreateRefEmbed.ts @@ -0,0 +1,16 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { CreateEvent } from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import _ from "lodash"; + +@ApplyOptions({ name: "create" }) +export default class extends GitHubEmbed { + public override run(event: CreateEvent, embed: EmbedBuilder) { + const type = _.capitalize(event.ref_type); + const updatedTitle = embed.data.title!.replace("{type}", type); + embed.setTitle(updatedTitle).setDescription(`${type}: **${event.ref}**`); + + return embed; + } +} diff --git a/src/github/embeds/Ref/DeleteRefEmbed.ts b/src/github/embeds/Ref/DeleteRefEmbed.ts new file mode 100644 index 00000000..8bae78bd --- /dev/null +++ b/src/github/embeds/Ref/DeleteRefEmbed.ts @@ -0,0 +1,16 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { CreateEvent } from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import _ from "lodash"; + +@ApplyOptions({ name: "delete" }) +export default class extends GitHubEmbed { + public override run(event: CreateEvent, embed: EmbedBuilder) { + const type = _.capitalize(event.ref_type); + const updatedTitle = embed.data.title!.replace("{type}", type); + embed.setTitle(updatedTitle).setDescription(`${type}: **${event.ref}**`); + + return embed; + } +} diff --git a/src/github/embeds/commits/CommitComment.ts b/src/github/embeds/commits/CommitComment.ts new file mode 100644 index 00000000..a686232e --- /dev/null +++ b/src/github/embeds/commits/CommitComment.ts @@ -0,0 +1,20 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { CommitCommentEvent } from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "commit_comment" }) +export default class extends GitHubEmbed { + public override run(event: CommitCommentEvent, embed: EmbedBuilder) { + const commit = event.comment.commit_id.slice(0, 7); + const commitUrl = `https://github.com/${event.repository.full_name}/commit/${event.comment.commit_id}`; + + embed + .setURL(event.comment.html_url) + .setDescription(event.comment.body.slice(0, EmbedLimits.MaximumDescriptionLength)) + .addFields([{ name: "On commit", value: `[\`${commit}\`](${commitUrl})` }]); + + return embed; + } +} diff --git a/src/github/embeds/PushEmbed.ts b/src/github/embeds/commits/PushEmbed.ts similarity index 63% rename from src/github/embeds/PushEmbed.ts rename to src/github/embeds/commits/PushEmbed.ts index 66c7dc7b..27160108 100644 --- a/src/github/embeds/PushEmbed.ts +++ b/src/github/embeds/commits/PushEmbed.ts @@ -1,7 +1,8 @@ -import { GitHubEmbed, type GitHubEmbedOptions } from "../lib/embed/structures/GitHubEmbed.js"; +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; import type { PushEvent } from "@octokit/webhooks-types"; import type { EmbedBuilder } from "discord.js"; -import { ApplyOptions } from "../lib/embed/decorators.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EMBED_COLORS } from "#github/lib/types.js"; @ApplyOptions({ name: "push" }) export default class extends GitHubEmbed { @@ -12,14 +13,17 @@ export default class extends GitHubEmbed { const refUrl = `${event.repository.svn_url}/tree/${id}`; const commits = event.commits.map( - (commit) => `[\`${commit.id.slice(0, 7)}\`](${commit.url}) ${commit.message} - ${commit.author.username || commit.author.name}` + (commit) => + `[\`${commit.id.slice(0, 7)}\`](${commit.url}) ${commit.message.split("\n")[0]} - ${commit.author.username || commit.author.name}` ); embed.setDescription(commits.join("\n").slice(0, 4096)); embed.addFields({ name: `On ${type}`, value: `[${id}](${refUrl})`.slice(0, 1024) }); const updatedTitle = embed.data.title!.replace(`{commit_count}`, commits.length.toString()); - embed.setTitle(`${updatedTitle}${commits.length === 1 ? "" : "s"}`); + embed.setTitle(`${updatedTitle}${commits.length === 1 ? "" : "s"} ${event.forced ? `(forced)` : ""}`.trim()); + + if (event.forced) embed.setColor(EMBED_COLORS.FAILED); return embed; } diff --git a/src/github/embeds/issues/IssueComment.ts b/src/github/embeds/issues/IssueComment.ts new file mode 100644 index 00000000..a471b9fc --- /dev/null +++ b/src/github/embeds/issues/IssueComment.ts @@ -0,0 +1,23 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { IssueCommentEvent } from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "issue_comment" }) +export default class extends GitHubEmbed { + public override run(event: IssueCommentEvent, embed: EmbedBuilder) { + if (event.action !== "created") return null; + + const isPr = Boolean(event.issue.pull_request); + const issue = `${event.issue.title} (#${event.issue.number})`; + + embed + .setTitle(`${event.repository.full_name} — ${isPr ? "Pull Request" : "Issue"} Comment Created`) + .setURL(event.comment.html_url) + .setDescription(event.comment.body.slice(0, EmbedLimits.MaximumDescriptionLength)) + .addFields([{ name: `On ${isPr ? "Pull Request" : "Issue"}`, value: `[${issue}](${event.issue.html_url})` }]); + + return embed; + } +} diff --git a/src/github/embeds/issues/IssueEmbed.ts b/src/github/embeds/issues/IssueEmbed.ts new file mode 100644 index 00000000..59d3a932 --- /dev/null +++ b/src/github/embeds/issues/IssueEmbed.ts @@ -0,0 +1,108 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { + IssuesAssignedEvent, + IssuesClosedEvent, + IssuesDemilestonedEvent, + IssuesEvent, + IssuesMilestonedEvent, + IssuesOpenedEvent, + IssuesReopenedEvent, + IssuesUnassignedEvent +} from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EMBED_COLORS } from "#github/lib/types.js"; +import _ from "lodash"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "issues" }) +export default class extends GitHubEmbed { + public override run(event: IssuesEvent, embed: EmbedBuilder) { + embed.setURL(event.issue.html_url); + + switch (event.action) { + case "opened": + case "reopened": + this.opened(event, embed); + break; + case "closed": + this.closed(event, embed); + break; + case "locked": + case "unlocked": + this.stageChange(event, embed); + break; + case "assigned": + case "unassigned": + this.assignUpdate(event, embed); + break; + case "demilestoned": + case "milestoned": + this.milestoneUpdate(event, embed); + break; + case "labeled": + case "unlabeled": + case "pinned": + case "unpinned": + case "deleted": + case "edited": + case "transferred": + return null; + default: + break; + } + + return embed; + } + + private opened(event: IssuesOpenedEvent | IssuesReopenedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${embed.data.title} #${event.issue.number}`) + .setDescription([`Title: **${event.issue.title}**\n`, `${event.issue.body}`].join("\n").slice(0, EmbedLimits.MaximumDescriptionLength)); + } + + private closed(event: IssuesClosedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${embed.data.title} #${event.issue.number}`) + .setDescription([`Title: **${event.issue.title}**`, `State: ${event.issue.active_lock_reason ?? "closed"}`].join("\n")); + } + + private stageChange(event: IssuesEvent, embed: EmbedBuilder) { + const state = event.action + .replace(/\_/g, " ") + .trim() + .toLowerCase() + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + embed + .setColor(EMBED_COLORS.UPDATE) + .setTitle(`${event.repository.full_name} — Issue #${event.issue.number}: Stage Update`) + .setDescription(`**${event.issue.title}**\nState: **${state}**`); + } + + private assignUpdate(event: IssuesAssignedEvent | IssuesUnassignedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${event.repository.full_name} — Issue #${event.issue.number}: User ${_.capitalize(event.action)}`) + .setDescription( + [ + `**${event.issue.title}**`, + `Action: \`${_.capitalize(event.action)}\``, + `Assignee: [${event.assignee?.login}](${event.assignee?.html_url})` + ].join("\n") + ); + } + + private milestoneUpdate(event: IssuesMilestonedEvent | IssuesDemilestonedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${event.repository.full_name} — Issue #${event.issue.number}`) + .setDescription( + [ + `**${event.issue.title}**`, + `Action: \`${_.capitalize(event.action)}\``, + `Milestone: [${event.milestone.title}](${event.milestone.html_url})` + ].join("\n") + ); + } +} diff --git a/src/github/embeds/pr/PullRequestEmbed.ts b/src/github/embeds/pr/PullRequestEmbed.ts new file mode 100644 index 00000000..41cfdf02 --- /dev/null +++ b/src/github/embeds/pr/PullRequestEmbed.ts @@ -0,0 +1,127 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { + PullRequestAssignedEvent, + PullRequestClosedEvent, + PullRequestDemilestonedEvent, + PullRequestEvent, + PullRequestMilestonedEvent, + PullRequestOpenedEvent, + PullRequestReopenedEvent, + PullRequestReviewRequestRemovedEvent, + PullRequestReviewRequestedEvent, + PullRequestUnassignedEvent +} from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EMBED_COLORS } from "#github/lib/types.js"; +import _ from "lodash"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "pull_request" }) +export default class extends GitHubEmbed { + public override run(event: PullRequestEvent, embed: EmbedBuilder) { + embed.setURL(event.pull_request.html_url); + + switch (event.action) { + case "opened": + case "reopened": + this.opened(event, embed); + break; + case "closed": + this.closed(event, embed); + break; + case "converted_to_draft": + case "ready_for_review": + case "locked": + case "unlocked": + this.stageChange(event, embed); + break; + case "assigned": + case "unassigned": + this.assignUpdate(event, embed); + break; + case "demilestoned": + case "milestoned": + this.milestoneUpdate(event, embed); + break; + case "review_requested": + case "review_request_removed": + this.reviewUpdate(event, embed); + break; + case "auto_merge_disabled": + case "auto_merge_enabled": + case "queued": + case "synchronize": + case "edited": + case "dequeued": + case "labeled": + case "unlabeled": + return null; + default: + break; + } + + return embed; + } + + private opened(event: PullRequestOpenedEvent | PullRequestReopenedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${embed.data.title} #${event.pull_request.number}`) + .setDescription( + [`Title: **${event.pull_request.title}**\n`, `${event.pull_request.body}`].join("\n").slice(0, EmbedLimits.MaximumDescriptionLength) + ); + } + + private closed(event: PullRequestClosedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${embed.data.title} #${event.pull_request.number}`) + .setDescription([`Title: **${event.pull_request.title}**`, `State: ${event.pull_request.merged ? "merged" : "closed"}`].join("\n")); + } + + private stageChange(event: PullRequestEvent, embed: EmbedBuilder) { + const state = event.action + .replace(/\_/g, " ") + .trim() + .toLowerCase() + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + embed + .setColor(EMBED_COLORS.UPDATE) + .setTitle(`${event.repository.full_name} — Pull Request #${event.pull_request.number}: Stage Update`) + .setDescription(`**${event.pull_request.title}**\nState: \`${state}\``); + } + + private assignUpdate(event: PullRequestAssignedEvent | PullRequestUnassignedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${event.repository.full_name} — Pull Request #${event.pull_request.number}: User ${_.capitalize(event.action)}`) + .setDescription([`**${event.pull_request.title}**`, `Assignee: [${event.assignee.login}](${event.assignee.html_url})`].join("\n")); + } + + private milestoneUpdate(event: PullRequestMilestonedEvent | PullRequestDemilestonedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${event.repository.full_name} — Pull Request #${event.pull_request.number}: ${_.capitalize(event.action)}`) + .setDescription([`**${event.pull_request.title}**`, `Milestone: [${event.milestone.title}](${event.milestone.html_url})`].join("\n")); + } + + private reviewUpdate(event: PullRequestReviewRequestedEvent | PullRequestReviewRequestRemovedEvent, embed: EmbedBuilder) { + if ("requested_reviewer" in event) { + const action = event.action + .replace(/\_/g, " ") + .trim() + .toLowerCase() + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + embed + .setTitle(`${event.repository.full_name} — Pull Request #${event.pull_request.number}: ${action}`) + .setDescription( + [`**${event.pull_request.title}**`, `Reviewer: [${event.requested_reviewer.login}](${event.requested_reviewer.html_url})`].join( + "\n" + ) + ); + } + } +} diff --git a/src/github/embeds/repository/CollaboratorEmbed.ts b/src/github/embeds/repository/CollaboratorEmbed.ts new file mode 100644 index 00000000..49e6f225 --- /dev/null +++ b/src/github/embeds/repository/CollaboratorEmbed.ts @@ -0,0 +1,14 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { MemberEvent } from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; + +@ApplyOptions({ name: "member" }) +export default class extends GitHubEmbed { + public override run(event: MemberEvent, embed: EmbedBuilder) { + const updatedTitle = embed.data.title!.replace("Member", "Collaborator"); + embed.setTitle(updatedTitle).setDescription(`Collaborator: [${event.member.login}](${event.member.html_url})`); + + return embed; + } +} diff --git a/src/github/embeds/repository/MilestoneEmbed.ts b/src/github/embeds/repository/MilestoneEmbed.ts new file mode 100644 index 00000000..726999b0 --- /dev/null +++ b/src/github/embeds/repository/MilestoneEmbed.ts @@ -0,0 +1,42 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { + MilestoneClosedEvent, + MilestoneCreatedEvent, + MilestoneDeletedEvent, + MilestoneEvent, + MilestoneOpenedEvent +} from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "milestone" }) +export default class extends GitHubEmbed { + public override run(event: MilestoneEvent, embed: EmbedBuilder) { + embed.setURL(event.milestone.html_url).setTitle(`${embed.data.title} #${event.milestone.number}`); + + switch (event.action) { + case "created": + case "opened": + this.opened(event, embed); + break; + case "closed": + case "deleted": + this.closed(event, embed); + break; + case "edited": + return null; + } + return embed; + } + + private opened(event: MilestoneCreatedEvent | MilestoneOpenedEvent, embed: EmbedBuilder) { + embed.setDescription( + [`Milestone: **${event.milestone.title}**`, event.milestone.description].join("\n").slice(0, EmbedLimits.MaximumDescriptionLength) + ); + } + + private closed(event: MilestoneClosedEvent | MilestoneDeletedEvent, embed: EmbedBuilder) { + embed.setDescription(`Milestone: **${event.milestone.title}**`); + } +} diff --git a/src/github/embeds/repository/PackageEmbed.ts b/src/github/embeds/repository/PackageEmbed.ts new file mode 100644 index 00000000..60d2cce9 --- /dev/null +++ b/src/github/embeds/repository/PackageEmbed.ts @@ -0,0 +1,19 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { PackageEvent } from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "package" }) +export default class extends GitHubEmbed { + public override run(event: PackageEvent, embed: EmbedBuilder) { + if (event.action !== "published") return null; + embed + .setURL(event.package.html_url) + .setDescription( + [`Package: **${event.package.name}**`, event.package.description ?? ""].join("\n").slice(0, EmbedLimits.MaximumDescriptionLength) + ); + + return embed; + } +} diff --git a/src/github/embeds/repository/ReleaseEmbed.ts b/src/github/embeds/repository/ReleaseEmbed.ts new file mode 100644 index 00000000..dad92626 --- /dev/null +++ b/src/github/embeds/repository/ReleaseEmbed.ts @@ -0,0 +1,39 @@ +import { GitHubEmbed, type GitHubEmbedOptions } from "#github/lib/embed/structures/GitHubEmbed.js"; +import type { + ReleaseCreatedEvent, + ReleaseEvent, + ReleasePrereleasedEvent, + ReleasePublishedEvent, + ReleaseReleasedEvent +} from "@octokit/webhooks-types"; +import type { EmbedBuilder } from "discord.js"; +import { ApplyOptions } from "#github/lib/embed/decorators.js"; +import { EmbedLimits } from "@sapphire/discord-utilities"; + +@ApplyOptions({ name: "release" }) +export default class extends GitHubEmbed { + public override run(event: ReleaseEvent, embed: EmbedBuilder) { + embed.setURL(event.release.html_url); + switch (event.action) { + case "created": + case "published": + case "released": + case "prereleased": + if (event.action === "created" && event.release.draft) return null; + this.published(event, embed); + break; + case "edited": + case "unpublished": + case "deleted": + return null; + } + + return embed; + } + + private published(event: ReleasePublishedEvent | ReleaseCreatedEvent | ReleaseReleasedEvent | ReleasePrereleasedEvent, embed: EmbedBuilder) { + embed + .setTitle(`${event.repository.full_name} — Release ${event.release.prerelease ? "Prereleased" : "Published"}: ${event.release.name}`) + .setDescription(event.release.body.slice(0, EmbedLimits.MaximumDescriptionLength)); + } +} diff --git a/src/github/lib/embed/BaseGitHubEmbed.ts b/src/github/lib/embed/BaseGitHubEmbed.ts index 1982a84d..f6e1a8ba 100644 --- a/src/github/lib/embed/BaseGitHubEmbed.ts +++ b/src/github/lib/embed/BaseGitHubEmbed.ts @@ -40,16 +40,17 @@ export default function getBaseGitHubEmbed({ author, repository, event }: GetBas .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); - if (event.name === "push") embed.setTitle(`${repository} — {commit_count} commit`); - else embed.setTitle(`${repository} — ${eventName}`); - switch (event.action ?? "") { + case ActionTypes.OPENED: case ActionTypes.COMPLETED: case ActionTypes.CREATED: case ActionTypes.FIXED: embed.setColor(EMBED_COLORS.SUCESS); break; case ActionTypes.CLOSED_BY_USER: + case ActionTypes.CLOSED: + embed.setColor(EMBED_COLORS.BLACK); + break; case ActionTypes.DELETED: embed.setColor(EMBED_COLORS.FAILED); break; @@ -57,11 +58,31 @@ export default function getBaseGitHubEmbed({ author, repository, event }: GetBas case ActionTypes.REOPENED: case ActionTypes.REOPENED_BY_USER: case ActionTypes.REREQUESTED: + case ActionTypes.UNASSIGNED: + case ActionTypes.ASSIGNED: embed.setColor(EMBED_COLORS.UPDATE); break; default: embed.setColor(EMBED_COLORS.DEFAULT); } + switch (event.name) { + case "push": + embed.setTitle(`${repository} — {commit_count} commit`); + break; + case "commit_comment": + embed.setTitle(`${repository} — Commit Comment Created`); + break; + case "create": + embed.setTitle(`${repository} — {type} Created`); + break; + case "delete": + embed.setTitle(`${repository} — {type} Deleted`); + embed.setColor(EMBED_COLORS.FAILED); + break; + default: + embed.setTitle(`${repository} — ${eventName}`); + } + return embed; } diff --git a/src/github/lib/embed/GitHubEmbedLoader.ts b/src/github/lib/embed/GitHubEmbedLoader.ts index 7058a335..34dd4d02 100644 --- a/src/github/lib/embed/GitHubEmbedLoader.ts +++ b/src/github/lib/embed/GitHubEmbedLoader.ts @@ -1,5 +1,5 @@ import { Collection, EmbedBuilder } from "discord.js"; -import type GitCordClient from "../../../discord/lib/GitCordClient.js"; +import type GitCordClient from "#discord/lib/GitCordClient.js"; import type { GitHubEmbed } from "./structures/GitHubEmbed.js"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -28,9 +28,9 @@ export default class GitHubEmbedLoader { * @param name The name of the event * @returns EmbedBuilder | null depending on the availability of the handlers */ - public async onEvent(payload: string, name: string): Promise { + public async onEvent(payload: string, name: string): Promise { const eventHandler = this.events.get(name); - if (!eventHandler) return null; + if (!eventHandler) return undefined; const embed = await eventHandler._run(JSON.parse(payload)); return embed; @@ -46,7 +46,7 @@ export default class GitHubEmbedLoader { const contents = await readdir(path); for await (const contentPath of contents) { // If the provided path is a folder, read out the folder - if (statSync(join(path, contentPath)).isDirectory()) results = await this.getFiles(path, results); + if (statSync(join(path, contentPath)).isDirectory()) results = await this.getFiles(join(path, contentPath), results); else results.push(join(path, contentPath)); } diff --git a/src/github/lib/embed/structures/GitHubEmbed.ts b/src/github/lib/embed/structures/GitHubEmbed.ts index b0809b09..9a095fab 100644 --- a/src/github/lib/embed/structures/GitHubEmbed.ts +++ b/src/github/lib/embed/structures/GitHubEmbed.ts @@ -18,7 +18,7 @@ export class GitHubEmbed implements GitHubEmbedOptions { * @param embed EmbedBuilder: Embed with default populated data (like author, title, color) * @returns Promise\ | EmbedBuilder */ - public run(event: WebhookEvent, embed: EmbedBuilder): Promise | EmbedBuilder { + public run(event: WebhookEvent, embed: EmbedBuilder): Promise | EmbedBuilder | null { this.logger.error(`${this.name}: GitHubEmbed does not have a valid run function.`); return embed; } @@ -39,7 +39,7 @@ export class GitHubEmbed implements GitHubEmbedOptions { username: sender.login, displayName: sender.name, profileImage: sender.avatar_url, - profileUrl: sender.url + profileUrl: sender.html_url }; } break; @@ -47,7 +47,7 @@ export class GitHubEmbed implements GitHubEmbedOptions { author = { username: event.sender.login, profileImage: event.sender.avatar_url, - profileUrl: event.sender.url + profileUrl: event.sender.html_url }; } } diff --git a/src/github/lib/types.ts b/src/github/lib/types.ts index ff2aed80..48ba4030 100644 --- a/src/github/lib/types.ts +++ b/src/github/lib/types.ts @@ -15,7 +15,11 @@ export enum ActionTypes { REREQUESTED = "rerequested", APPEARED_IN_BRANCH = "appeared_in_branch", CLOSED_BY_USER = "closed_by_user", + CLOSED = "closed", FIXED = "fixed", + ASSIGNED = "assigned", + UNASSIGNED = "unassigned", + OPENED = "opened", REOPENED = "reopened", REOPENED_BY_USER = "reopened_by_user" } @@ -24,7 +28,8 @@ export const EMBED_COLORS = { SUCESS: "#3bf77a", FAILED: "#e72525", DEFAULT: "#3b7ff7", - UPDATE: "#3e3bf7" + UPDATE: "#3e3bf7", + BLACK: "#1f1f1f" } as const; export const BLOCKED_EVENTS: WebhookEventName[] = [ diff --git a/src/github/lib/webhook/GitHubWebhookManager.ts b/src/github/lib/webhook/GitHubWebhookManager.ts index af5758b4..c317f46c 100644 --- a/src/github/lib/webhook/GitHubWebhookManager.ts +++ b/src/github/lib/webhook/GitHubWebhookManager.ts @@ -5,9 +5,13 @@ import type GitHubManager from "../GitHubManager.js"; import express, { type Request, type Response } from "express"; import GitCordGuildWebhook from "#database/structures/GuildWebhook.js"; import GitCordGuild from "#database/structures/Guild.js"; -import axios from "axios"; +import { GITHUB_AVATAR_URL } from "#shared/constants.js"; +import { ChannelType } from "discord.js"; +import { RequestManager, RequestMethod } from "@discordjs/rest"; export default class GitHubWebhookManager { + public requestManager = new RequestManager({}); + public constructor(public client: GitCordClient, public manager: GitHubManager) {} public init() { @@ -81,6 +85,7 @@ export default class GitHubWebhookManager { } await this.receiveEvent(req.body, deliveryId, event, signature, webhook); + res.sendStatus(200); } /** Parses the incoming event data */ @@ -89,12 +94,38 @@ export default class GitHubWebhookManager { if (!isValid) return; const embed = await this.manager.embedLoader.onEvent(payload, name); + if (embed === null) return; + if (embed) { - await webhook.discordWebhook.send({ embeds: [embed] }); + let threadName: string | undefined; + if (webhook.type === "FORUM") { + const parsedPayload = JSON.parse(payload); + if ("repository" in parsedPayload) threadName = parsedPayload.repository.full_name as string; + + const channel = await this.client.channels.fetch(webhook.id); + if (!channel) return; + + if (channel.type === ChannelType.GuildForum && !channel.threads.cache.get(threadName!)) { + await channel.threads.create({ + name: threadName!, + message: { content: `GitHub Notifications for **${threadName}**: https://github.com/${threadName}` } + }); + } + } + + await webhook.discordWebhook + .send({ embeds: [embed], avatarURL: GITHUB_AVATAR_URL, username: "GitCord", threadName }) + .catch((err) => this.client.logger.error(err)); return; } - await this.forwardEvent(payload, deliveryId, name, signature, `${webhook.discordUrl}/github`); + await this.forwardEvent( + payload, + deliveryId, + name, + signature, + `/webhooks/${webhook.discordWebhook.id}/${webhook.discordWebhook.token}/github` + ); } /** Verifies if the received event is valid and coming from GitHub */ @@ -106,10 +137,10 @@ export default class GitHubWebhookManager { } /** Forward the event data if no applicable event handler is found */ - private async forwardEvent(payload: string, deliveryId: string, name: string, signature: string, webhook: string) { - const headers = { ContentType: "application/json", "X-Github-Event": name, "X-Github-Delivery": deliveryId, "X-Hub-Signature": signature }; - await axios.post(webhook, payload, { - headers - }); + private async forwardEvent(payload: string, deliveryId: string, name: string, signature: string, webhook: `/${string}/github`) { + const headers = { "Content-Type": "application/json", "X-Github-Event": name, "X-Github-Delivery": deliveryId, "X-Hub-Signature": signature }; + await this.requestManager + .setToken("null") + .queueRequest({ method: RequestMethod.Post, fullRoute: webhook, body: JSON.parse(payload), headers }); } } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9b1deb09..f4587341 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -8,3 +8,5 @@ export const BASE_BOT_DIR = join(__dirname, "..", "discord", "bot"); export const BOT_COMMANDS_DIR = join(BASE_BOT_DIR, "commands"); export const BOT_LISTENER_DIR = join(BASE_BOT_DIR, "listeners"); export const BOT_INTERACTIONS_DIR = join(BASE_BOT_DIR, "interactions"); + +export const GITHUB_AVATAR_URL = "https://cdn.ijskoud.dev/files/2zVGPBN3ZmId.webp"; diff --git a/yarn.lock b/yarn.lock index 639bc5b7..da1908bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -725,6 +725,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.14.194": + version: 4.14.194 + resolution: "@types/lodash@npm:4.14.194" + checksum: 113f34831c461469d91feca2dde737f88487732898b4d25e9eb23b087bb193985f864d1e1e0f3b777edc5022e460443588b6000a3b2348c966f72d17eedc35ea + languageName: node + linkType: hard + "@types/mime@npm:*": version: 3.0.1 resolution: "@types/mime@npm:3.0.1" @@ -2607,6 +2614,7 @@ __metadata: "@snowcrystals/iglo": next "@types/eventsource": ^1.1.11 "@types/express": ^4.17.17 + "@types/lodash": ^4.14.194 "@types/node": ^18.16.0 "@typescript-eslint/eslint-plugin": ^5.59.0 "@typescript-eslint/parser": ^5.59.0 @@ -2622,6 +2630,7 @@ __metadata: husky: ^8.0.3 is-ci: ^3.0.1 lint-staged: ^13.2.1 + lodash: ^4.17.21 nodemon: ^2.0.22 prettier: ^2.8.8 prisma: ^4.13.0