-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2aa824c
Showing
47 changed files
with
2,357 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 })); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} |
Oops, something went wrong.