Skip to content

Commit

Permalink
feat: add files
Browse files Browse the repository at this point in the history
  • Loading branch information
vitoUwu committed Nov 9, 2024
0 parents commit 2aa824c
Show file tree
Hide file tree
Showing 47 changed files with 2,357 additions and 0 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Deco Bot

O **Deco Bot** é um bot para Discord projetado para integrar os projetos da Deco
com o servidor do Discord.

## Tecnologias

- **TypeScript**
- **Deno**
86 changes: 86 additions & 0 deletions actions/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { STATUS_CODE } from "@std/http/status";
import {
type DiscordInteraction,
InteractionTypes,
verifySignature,
} from "../deps/discordeno.ts";
import type { AppContext } from "../mod.ts";
import buttons from "../sdk/discord/buttons/index.ts";
import commands from "../sdk/discord/commands/index.ts";
import ping from "../sdk/discord/commands/ping.ts";
import { Interaction } from "../sdk/discord/lib.ts";

export default function action(
props: DiscordInteraction,
req: Request,
ctx: AppContext,
) {
if (req.method === "POST") {
const signature = req.headers.get("x-signature-ed25519") || "";
const timestamp = req.headers.get("x-signature-timestamp") || "";
const publicKey = ctx.discord.public_key;
const rawBody = JSON.stringify(props);

const { isValid } = verifySignature({
signature,
timestamp,
publicKey,
body: rawBody,
});

if (!isValid) {
return new Response("Invalid signature", {
status: STATUS_CODE.Unauthorized,
});
}
}

const interaction = new Interaction(props, ctx.discord.bot);

if (interaction.type === InteractionTypes.Ping) {
return ping();
}

if (!interaction.data) {
return new Response(null, { status: STATUS_CODE.BadRequest });
}

if (interaction.isApplicationCommandInteraction()) {
const command = commands.get(interaction.data.name);

if (!command) {
return new Response(null, { status: STATUS_CODE.NotFound });
}

return command.execute(
interaction,
req,
ctx,
).catch((err) => {
console.error(err);
return new Response(null, { status: STATUS_CODE.InternalServerError });
}).finally(() => new Response(null, { status: STATUS_CODE.OK }));
}

if (interaction.isButtonInteraction()) {
const buttonData = interaction.parseCustomId();
if (!buttonData) {
return new Response(null, { status: STATUS_CODE.BadRequest });
}

const button = buttons.get(buttonData.id);

if (!button) {
return new Response(null, { status: STATUS_CODE.NotFound });
}

return button.execute(
interaction,
req,
ctx,
).catch((err) => {
console.error(err);
return new Response(null, { status: STATUS_CODE.InternalServerError });
}).finally(() => new Response(null, { status: STATUS_CODE.OK }));
}
}
23 changes: 23 additions & 0 deletions actions/notify/reviewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { sendMessage } from "../../deps/discordeno.ts";
import type { AppContext } from "../../mod.ts";
import { userMention } from "../../sdk/discord/textFormatting.ts";
import type { ProjectUser } from "../../types.ts";

export interface Props {
channelId: string;
reviewer: ProjectUser;
}

export default async function action(
{ channelId, reviewer }: Props,
_req: Request,
ctx: AppContext,
) {
await sendMessage(ctx.discord.bot, channelId, {
content: `Pode confirmar o pedido de revisão? ${
userMention(
reviewer.discordId,
)
}`,
}).catch(console.error);
}
86 changes: 86 additions & 0 deletions actions/summary/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { STATUS_CODE } from "@std/http/status";
import { type Embed, sendMessage } from "../../deps/discordeno.ts";
import type { AppContext } from "../../mod.ts";
import { dateInSeconds, isToday } from "../../sdk/date.ts";
import { inlineCode, timestamp } from "../../sdk/discord/textFormatting.ts";
import { isDraft } from "../../sdk/github/utils.ts";

export default async function action(
_props: unknown,
req: Request,
ctx: AppContext,
) {
if (req.method !== "GET") {
return new Response(null, { status: STATUS_CODE.MethodNotAllowed });
}

const secret = req.headers.get("x-cron-secret");
if (!secret || secret !== ctx.cronJobSecret?.get()) {
return new Response(null, { status: STATUS_CODE.Unauthorized });
}

for (const project of ctx.projects.filter((project) => project.active)) {
const openPullRequests = (await ctx.githubClient.getPullRequests({
owner: project.github.org_name,
repo: project.github.repo_name,
state: "open",
direction: "desc",
sort: "created",
})).filter((pr) => !isDraft(pr.title));

const closedPullRequests = (await ctx.githubClient.getPullRequests({
owner: project.github.org_name,
repo: project.github.repo_name,
state: "closed",
direction: "desc",
sort: "updated",
})).filter((pr) => pr.closed_at && isToday(pr.closed_at));

if (!openPullRequests.length && !closedPullRequests.length) {
continue;
}

const embeds: Embed[] = [
{
title: `${project.github.org_name}/${project.github.repo_name}`,
description: "Resumo das pull requests de hoje",
},
];

if (openPullRequests.length) {
embeds.push({
title: `(${openPullRequests.length}) Pull requests abertos`,
color: 0x02c563,
fields: openPullRequests.slice(0, 10).map((pr) => ({
name: `${pr.number} | ${pr.title}`,
value: `Criado ${
timestamp(dateInSeconds(pr.created_at), "R")
}\nCriado por ${
inlineCode(pr.user?.login ?? "No user")
}\n[Ver no GitHub](${pr.html_url})`,
inline: false,
})),
});
}

if (closedPullRequests.length) {
embeds.push({
title: `(${closedPullRequests.length}) Pull requests mergeados hoje`,
color: 0x8957e5,
fields: closedPullRequests.slice(0, 10).map((pr) => ({
name: `${pr.number} | ${pr.title}`,
value: `Fechado ${
timestamp(dateInSeconds(pr.updated_at), "R")
}\nCriado por ${
inlineCode(pr.user?.login ?? "No user")
}\n[Ver no GitHub](${pr.html_url})`,
inline: false,
})),
});
}

sendMessage(ctx.discord.bot, project.discord.summary_channel_id, {
embeds,
});
}
}
28 changes: 28 additions & 0 deletions actions/updateCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { STATUS_CODE } from "@std/http/status";
import { upsertGlobalApplicationCommands } from "../deps/discordeno.ts";
import type { AppContext } from "../mod.ts";
import commands from "../sdk/discord/commands/index.ts";

export default async function action(
_props: unknown,
_req: unknown,
ctx: AppContext,
) {
try {
await upsertGlobalApplicationCommands(
ctx.discord.bot,
[...commands.values()].map((command) => command.data),
);
} catch (err) {
return new Response(
JSON.stringify({
error: (err as Error).message,
}),
{
status: STATUS_CODE.InternalServerError,
},
);
}

return new Response(null, { status: STATUS_CODE.NoContent });
}
141 changes: 141 additions & 0 deletions actions/webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { STATUS_CODE } from "@std/http/status";
import type { AppContext } from "../mod.ts";
import onIssueClosed from "../sdk/github/events/onIssueClosed.ts";
import onIssueOpened from "../sdk/github/events/onIssueOpened.ts";
import onIssueReopened from "../sdk/github/events/onIssueReopened.ts";
import onPullRequestMerge from "../sdk/github/events/onPullRequestMerge.ts";
import onPullRequestOpen from "../sdk/github/events/onPullRequestOpen.ts";
import onReviewRequested from "../sdk/github/events/onReviewRequested.ts";
import onReviewSubmitted from "../sdk/github/events/onReviewSubmitted.ts";
import type { WebhookEvent } from "../sdk/github/types.ts";
import { wasInDraft } from "../sdk/github/utils.ts";
import {
isIssuesEvent,
isPingEvent,
isPullRequestEvent,
isPullRequestReviewEvent,
} from "../sdk/github/validateWebhookPayload.ts";
import { verify } from "../sdk/github/verifyWebhook.ts";

export default async function action(
props: WebhookEvent,
req: Request,
ctx: AppContext,
) {
const signature = req.headers.get("x-hub-signature-256");

if (!signature) {
console.error("Signature is missing. Request Headers:", req.headers);
return new Response("Signature is missing", {
status: STATUS_CODE.Unauthorized,
});
}

if (!("repository" in props) || !props.repository) {
console.error("Repository is missing. Request Body:", props);
return new Response("Repository is missing", {
status: STATUS_CODE.BadRequest,
});
}

const eventName = req.headers.get("x-github-event");
if (!eventName) {
console.error("Event name is missing. Request Headers:", req.headers);
return new Response("Event name is missing", {
status: STATUS_CODE.BadRequest,
});
}

if (isPingEvent(eventName, props)) {
return new Response(null, { status: 200 });
}

const project = ctx.projects.find(({ github }) =>
`${github.org_name}/${github.repo_name}` === props.repository!.full_name
);
if (!project) {
console.error("Unknown repository. Request Body:", props);
return new Response("Unknown repository", {
status: STATUS_CODE.BadRequest,
});
}

if (!project.active) {
console.error("Project is not active. Data:", { project, props });
return new Response("Project is not active", {
status: STATUS_CODE.ServiceUnavailable,
});
}

const secret = project.github.webhook_secret?.get();
// if (!secret) {
// console.error("Secret is missing. Data:", { project, props });
// return new Response("Secret is missing", {
// status: STATUS_CODE.BadRequest,
// });
// }

if (secret && !(await verify(secret, JSON.stringify(props), signature))) {
console.error("Invalid signature. Data:", { project, props });
return new Response("Invalid signature", {
status: STATUS_CODE.Unauthorized,
});
}

if (isPullRequestEvent(eventName, props)) {
if (props.action === "opened" || wasInDraft(props)) {
return onPullRequestOpen(props, project, ctx);
}

if (props.action === "closed" && props.pull_request.merged) {
return onPullRequestMerge(props, project, ctx);
}

if (props.action === "review_requested") {
return onReviewRequested(props, project, ctx);
}

console.warn("Unhandled action. Data:", {
action: props.action,
project,
props,
});
return new Response(null, { status: STATUS_CODE.NoContent });
}

if (isPullRequestReviewEvent(eventName, props)) {
if (props.action === "submitted") {
return onReviewSubmitted(props, project, ctx);
}

console.warn("Unhandled action. Data:", {
action: props.action,
project,
props,
});
return new Response(null, { status: STATUS_CODE.NoContent });
}

if (isIssuesEvent(eventName, props)) {
if (props.action === "opened") {
return onIssueOpened(props, project, ctx);
}

if (props.action === "closed" && props.issue.state_reason === "completed") {
return onIssueClosed(props, project, ctx);
}

if (props.action === "reopened") {
return onIssueReopened(props, project, ctx);
}

console.warn("Unhandled action. Data:", {
action: props.action,
project,
props,
});
return new Response(null, { status: STATUS_CODE.NoContent });
}

return new Response(null, { status: 200 });
}
Loading

0 comments on commit 2aa824c

Please sign in to comment.