Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.
Merged
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
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts": {
"build": "tsc --build",
"build:watch": "tsc --watch",
"start:dev": "nodemon ./dist",
"start:dev": "NODE_ENV=development nodemon ./dist",
"start": "node ./dist",
"lint": "eslint src",
"postinstall": "is-ci || husky install"
Expand All @@ -21,6 +21,8 @@
"@sapphire/eslint-config": "^4.4.1",
"@sapphire/prettier-config": "^1.4.5",
"@sapphire/ts-config": "^4.0.0",
"@types/eventsource": "^1.1.11",
"@types/express": "^4.17.17",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
Expand All @@ -39,11 +41,15 @@
},
"packageManager": "yarn@3.5.0",
"dependencies": {
"@octokit/webhooks": "^11.0.0",
"@octokit/webhooks-types": "^6.11.0",
"@snowcrystals/icicle": "^1.0.3",
"@snowcrystals/iglo": "^1.2.1",
"colorette": "2.0.19",
"discord.js": "^14.9.0",
"dotenv": "^16.0.3",
"eventsource": "^2.0.2",
"express": "^4.18.2",
"zod": "^3.21.4"
}
}
26 changes: 26 additions & 0 deletions src/github/embeds/PushEmbed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { GitHubEmbed, type GitHubEmbedOptions } from "../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";

@ApplyOptions<GitHubEmbedOptions>({ name: "push" })
export default class extends GitHubEmbed {
public override run(event: PushEvent, embed: EmbedBuilder) {
const [, _type, ..._id] = event.ref.split(/\//g);
const type = _type === "tags" ? "tag" : "branch";
const id = _id.join("/");

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}`
);

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"}`);

return embed;
}
}
18 changes: 18 additions & 0 deletions src/github/lib/GitHubManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type GitCordClient from "../../lib/GitCordClient.js";
import GitHubEmbedLoader from "./embed/GitHubEmbedLoader.js";
import GitHubWebhookManager from "./webhook/GitHubWebhookManager.js";

export default class GitHubManager {
public webhookManager: GitHubWebhookManager;
public embedLoader: GitHubEmbedLoader;

public constructor(public client: GitCordClient) {
this.webhookManager = new GitHubWebhookManager(client, this);
this.embedLoader = new GitHubEmbedLoader(client);
}

public async init() {
this.webhookManager.init();
await this.embedLoader.init();
}
}
67 changes: 67 additions & 0 deletions src/github/lib/embed/BaseGitHubEmbed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { WebhookEventName } from "@octokit/webhooks-types";
import { EmbedBuilder } from "discord.js";
import { ActionTypes, EMBED_COLORS } from "../types.js";

export interface GitHubEventSender {
/** The username of the sender (e.g.: ijsKoud) */
username: string;
/** The display name of the sender (e.g.: Daan Klarenbeek) */
displayName?: string;
/** The profile url of the sender */
profileUrl: string;
/** The profile image url of the sender */
profileImage: string;
}

export interface GitHubEventType {
/** The name of the event */
name: WebhookEventName;
/** The action type (e.g.: created, removed, edited) */
action?: string;
}

export interface GetBaseGitHubEmbedOptions {
author?: GitHubEventSender;
repository?: string;
event: GitHubEventType;
}

export default function getBaseGitHubEmbed({ author, repository, event }: GetBaseGitHubEmbedOptions): EmbedBuilder {
const embed = new EmbedBuilder();
if (author) {
const name = author.displayName ? `${author.username} (${author.displayName})` : author.username;
embed.setAuthor({ name, iconURL: author.profileImage, url: author.profileUrl });
}

const eventName = `${event.name.replace(/\_/g, " ")} ${event.action ?? ""}`
.trim()
.toLowerCase()
.split(" ")
.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.COMPLETED:
case ActionTypes.CREATED:
case ActionTypes.FIXED:
embed.setColor(EMBED_COLORS.SUCESS);
break;
case ActionTypes.CLOSED_BY_USER:
case ActionTypes.DELETED:
embed.setColor(EMBED_COLORS.FAILED);
break;
case ActionTypes.EDITED:
case ActionTypes.REOPENED:
case ActionTypes.REOPENED_BY_USER:
case ActionTypes.REREQUESTED:
embed.setColor(EMBED_COLORS.UPDATE);
break;
default:
embed.setColor(EMBED_COLORS.DEFAULT);
}

return embed;
}
78 changes: 78 additions & 0 deletions src/github/lib/embed/GitHubEmbedLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Collection, EmbedBuilder } from "discord.js";
import type GitCordClient from "../../../lib/GitCordClient.js";
import type { GitHubEmbed } from "./structures/GitHubEmbed.js";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { readdir } from "node:fs/promises";
import { statSync } from "node:fs";
import { underline } from "colorette";

const __dirname = dirname(fileURLToPath(import.meta.url));

export default class GitHubEmbedLoader {
public events = new Collection<string, GitHubEmbed>();
public directory = join(__dirname, "..", "..", "embeds");

public constructor(public client: GitCordClient) {}

/** Start the GitHub Embed creator process */
public async init() {
const files = await this.getFiles(this.directory);
const count = await this.loadFiles(files);
this.client.logger.debug(`[GitHubEmbedLoader]: ${count} files loaded`);
}

/**
* Runs the embed creator for the provided event if one is found
* @param payload The event payload
* @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<EmbedBuilder | null> {
const eventHandler = this.events.get(name);
if (!eventHandler) return null;

const embed = await eventHandler._run(JSON.parse(payload));
return embed;
}

/**
* Gets all the files from a directory
* @param path The filepath of a directory to read
* @param results optional array of already processed results
* @returns Array of file paths
*/
private async getFiles(path: string, results: string[] = []) {
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);
else results.push(join(path, contentPath));
}

return results;
}

/**
* Load the Embed creators of the provided filePaths
* @param filePaths The files to load
* @returns The amount of files which were loaded
*/
private async loadFiles(filePaths: string[]) {
let count = 0;
for await (const filePath of filePaths.filter((filepath) => filepath.endsWith(".js"))) {
try {
const { default: GitHubEmbedConstructor } = (await import(filePath)) as { default: new () => GitHubEmbed };
const Embed = new GitHubEmbedConstructor();

this.events.set(Embed.name, Embed);
count++;
} catch (err) {
this.client.logger.error(`[GitHubEmbedLoader]: Failed to load file with path ${underline(filePath)}`);
this.client.logger.error(err);
}
}

return count;
}
}
13 changes: 13 additions & 0 deletions src/github/lib/embed/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createClassDecorator, type ApplyOptionsParam, createProxy, type ConstructorType } from "@snowcrystals/iglo";

/**
* Applies the ConstructorOptions to a class
* @param result The ConstructorOptions or a function to get the ConstructorOptions from
*/
export function ApplyOptions<Options>(result: ApplyOptionsParam<Options>): ClassDecorator {
return createClassDecorator((target: ConstructorType) =>
createProxy(target, {
construct: (constructor, [baseOptions = {}]: [Partial<Options>]) => new constructor({ ...baseOptions, ...result })
})
);
}
72 changes: 72 additions & 0 deletions src/github/lib/embed/structures/GitHubEmbed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { User, WebhookEvent, WebhookEventName } from "@octokit/webhooks-types";
import { Logger } from "@snowcrystals/icicle";
import type { EmbedBuilder } from "discord.js";
import getBaseGitHubEmbed, { type GitHubEventSender, type GitHubEventType } from "../BaseGitHubEmbed.js";
import { BLOCKED_EVENTS, UserTypes, type WebhookEvents } from "../../types.js";

export class GitHubEmbed implements GitHubEmbedOptions {
public name: WebhookEventName;
public logger = new Logger();

public constructor(options: GitHubEmbedOptions) {
this.name = options.name;
}

/**
* The function which run process the incoming webhook data
* @param event WebhookEvent: The event coming from GitHub
* @param embed EmbedBuilder: Embed with default populated data (like author, title, color)
* @returns Promise\<EmbedBuilder\> | EmbedBuilder
*/
public run(event: WebhookEvent, embed: EmbedBuilder): Promise<EmbedBuilder> | EmbedBuilder {
this.logger.error(`${this.name}: GitHubEmbed does not have a valid run function.`);
return embed;
}

public _run(event: WebhookEvents) {
// Block events which aren't applicable for webhooks
if (BLOCKED_EVENTS.includes(this.name)) return null;

let author: GitHubEventSender | undefined = undefined;
let repository = "";

if ("sender" in event) {
switch (event.sender.type) {
case UserTypes.USER:
{
const sender = event.sender as User;
author = {
username: sender.login,
displayName: sender.name,
profileImage: sender.avatar_url,
profileUrl: sender.url
};
}
break;
default:
author = {
username: event.sender.login,
profileImage: event.sender.avatar_url,
profileUrl: event.sender.url
};
}
}

// Assign repository name and otherwise name of organization
if ("repository" in event && typeof event.repository !== "undefined") repository = event.repository.full_name;
else if ("organization" in event && typeof event.organization !== "undefined") repository = event.organization.login;

const eventType: GitHubEventType = {
name: this.name,
action: "action" in event ? event.action : undefined
};

const embed = getBaseGitHubEmbed({ author, repository, event: eventType });
return this.run(event, embed);
}
}

export interface GitHubEmbedOptions {
/** The name of the event you are listening to */
name: WebhookEventName;
}
52 changes: 52 additions & 0 deletions src/github/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { EventPayloadMap, WebhookEventName } from "@octokit/webhooks-types";

/** Type names of possible GitHub users */
export enum UserTypes {
USER = "User"
}

/** Types of possible GitHub actions */
export enum ActionTypes {
CREATED = "created",
DELETED = "deleted",
EDITED = "edited",
COMPLETED = "completed",
REQUESTED_ACTION = "requested_action",
REREQUESTED = "rerequested",
APPEARED_IN_BRANCH = "appeared_in_branch",
CLOSED_BY_USER = "closed_by_user",
FIXED = "fixed",
REOPENED = "reopened",
REOPENED_BY_USER = "reopened_by_user"
}

export const EMBED_COLORS = {
SUCESS: "#3bf77a",
FAILED: "#e72525",
DEFAULT: "#3b7ff7",
UPDATE: "#3e3bf7"
} as const;

export const BLOCKED_EVENTS: WebhookEventName[] = [
"github_app_authorization",
"installation",
"installation_repositories",
"installation_target",
"marketplace_purchase",
"membership",
"ping",
"status"
];

export type EventNames = keyof Omit<
EventPayloadMap,
| "github_app_authorization"
| "installation"
| "installation_repositories"
| "installation_target"
| "marketplace_purchase"
| "membership"
| "ping"
| "status"
>;
export type WebhookEvents = EventPayloadMap[EventNames];
Loading