feat: migrate away from trello (#2)

Closes #1

Reviewed-on: https://codeberg.org/nhcarrigan/a4p-bot/pulls/2
Co-authored-by: Naomi <commits@nhcarrigan.com>
Co-committed-by: Naomi <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2024-05-21 05:11:11 +00:00 committed by Naomi Carrigan
parent bbd6a710bb
commit a85b478dba
16 changed files with 2182 additions and 1856 deletions

View File

@ -15,7 +15,7 @@
},
"engines": {
"node": "20",
"pnpm": "8"
"pnpm": "9"
},
"keywords": [
"template",

3614
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("MONGO_URI")
}
model rewards {
id String @id @default(auto()) @map("_id") @db.ObjectId
trelloId String
messageId String
createdAt Int @default(0)
claimedBy String @default("") // user who claimed
completed Boolean @default(false)
@@unique([trelloId], map: "trello")
@@unique([messageId], map: "message")
@@index([claimedBy, completed])
@@index([claimedBy])
}

View File

@ -41,6 +41,4 @@ export const TrelloComments: {
[Webhooks.NewCommissions]: (userName) => `Artwork claimed by ${userName}.`,
[Webhooks.CompleteCommissions]: (userName) =>
`Distribution claimed by ${userName}.`,
[Webhooks.NewTest]: (userName) => `Artwork claimed by ${userName}.`,
[Webhooks.CompleteTest]: (userName) => `Distribution claimed by ${userName}.`,
};

View File

@ -1,25 +1,19 @@
import { Message } from "discord.js";
import { User } from "discord.js";
export enum Webhooks {
NewCommissions = "1172850885571911760",
CompleteCommissions = "1173062416041525259",
NewTest = "1173009530511171634",
CompleteTest = "946487942526935060",
}
/**
* Grabs the message from Discord, formats it based on the webhook ID.
* Grabs the user from Discord, formats it based on the webhook ID.
*
* @param {Message} message The message payload from Discord.
* @param {User} u The user payload from Discord.
* @returns {string} The formatted text to send.
*/
export const DMTexts: { [key in Webhooks]: (message: Message) => string } = {
[Webhooks.NewCommissions]: (message) =>
`[Here is the donation commission you claimed~!](${message.url})\n> ${message.content}`,
[Webhooks.CompleteCommissions]: (message) =>
`[Thank you for agreeing to deliver the following commission~!](${message.url})\n> ${message.content}`,
[Webhooks.NewTest]: (message) =>
`[Here is the donation commission you claimed~!](${message.url})\n> ${message.content}`,
[Webhooks.CompleteTest]: (message) =>
`[Thank you for agreeing to deliver the following commission~!](${message.url})\n> ${message.content}`,
export const ClaimTexts: { [key in Webhooks]: (u: User) => string } = {
[Webhooks.NewCommissions]: (u) =>
`Thanks for agreeing to do this art, <@!${u.id}>~! When it's completed, upload the image here and I'll automatically close this thread!`,
[Webhooks.CompleteCommissions]: (u) =>
`Thanks for agreeing to deliver this art, <@!${u.id}>~! When it's completed, upload a confirmation image here and I'll automatically close this thread!`,
};

View File

@ -1,4 +1,9 @@
import { Message, PermissionFlagsBits } from "discord.js";
import {
ChannelType,
Message,
PermissionFlagsBits,
TextChannel,
} from "discord.js";
import { ExtendedClient } from "../interface/ExtendedClient";
import { logTicketMessage } from "../modules/logTicketMessage";
@ -106,6 +111,48 @@ export const onMessageCreate = async (
}
}
}
if (message.channel.type === ChannelType.PublicThread) {
if (message.channel.parentId === "1172568787330019340") {
const { attachments } = message;
if (!attachments.size) {
return;
}
await message.reply({
content:
"Accepting this as submission and closing out art request. Thanks for your support!",
});
const starter = await message.channel.fetchStarterMessage();
const distChannel = (await message.channel.guild.channels.fetch(
"1173061747737903315"
)) as TextChannel;
if (!starter || !distChannel) {
await message.channel.send(
"<@!465650873650118659>, couldn't port this to distributor channel. Please port manually~!"
);
await message.channel.setArchived(true);
return;
}
await message.channel.setArchived(true);
await distChannel.send({
content: starter.content,
files: [...attachments.values()],
});
}
if (message.channel.parentId === "1173061747737903315") {
const { attachments } = message;
if (!attachments.size) {
return;
}
await message.reply({
content:
"Accepting this as delivery confirmation and closing out distribution request. Thanks for your support!",
});
await message.channel.setArchived(true);
}
}
return;
}
if (message.author.bot && !isValidWebhook(message.author.id)) {

View File

@ -2,11 +2,12 @@ import {
MessageReaction,
PartialMessageReaction,
PartialUser,
TextChannel,
ThreadAutoArchiveDuration,
User,
} from "discord.js";
import { TrelloComments } from "../config/Trello";
import { DMTexts } from "../config/Webhooks";
import { ClaimTexts } from "../config/Webhooks";
import { ExtendedClient } from "../interface/ExtendedClient";
import { isValidWebhook } from "../utils/isValidWebhook";
@ -49,58 +50,24 @@ export const onReactionAdd = async (
await r.users.remove(user.id);
return;
}
const claimedByUser = await bot.db.rewards.findMany({
where: {
claimedBy: user.id,
completed: false,
},
});
if (claimedByUser.length >= 2) {
const channel = r.message.channel as TextChannel;
const claimedByUser = (await channel.threads.fetchActive()).threads.filter(
(t) => t.name === user.id
);
if (claimedByUser.size >= 2) {
await r.users.remove(user.id);
await user.send(
"You currently have 2 open art rewards. Please do not claim another until you have completed one of your outstanding art works."
);
return;
}
const files = [...message.attachments.values()];
await user
.send({
content: DMTexts[message.author.id](message),
files,
})
.catch(async (err) => {
await bot.debug.send({
content: `[Failed to DM ${user.username}:](<${r.message.url}>) ${err.message}`,
});
});
const trelloId =
message.content.split("You can ignore this it's just for the bot. ")[1] ||
message.content.split(
"You can ignore this ID it's just for the bot: "
)[1];
if (trelloId) {
await fetch(
`https://api.trello.com/1/cards/${trelloId}/actions/comments?text=${TrelloComments[
message.author.id
](user.displayName || user.username || user.id)}&key=${
process.env.TRELLO_KEY
}&token=${process.env.TRELLO_TOKEN}`,
{
method: "POST",
headers: {
accept: "application/json",
},
}
);
await bot.db.rewards.update({
where: {
trelloId,
},
data: {
claimedBy: user.id,
},
});
}
const thread = await channel.threads.create({
autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek,
name: user.id,
});
await thread.members.add(user.id);
await thread.send(ClaimTexts[message.author.id](user as User));
} catch (err) {
await bot.debug.send(
`Error in processing new reaction: ${(err as Error).message}`

View File

@ -4,8 +4,9 @@ import { ExtendedClient } from "../interface/ExtendedClient";
import { checkAirtableRecords } from "../modules/checkAirtableRecords";
import { fetchMessages } from "../modules/messages/fetchMessages";
import { sendUnclaimedArt } from "../modules/reminders/sendUnclaimedArt";
import { sendUnclaimedDistros } from "../modules/reminders/sendUnclaimedDistros";
import { sendUnfinishedArt } from "../modules/reminders/sendUnfinishedArt";
import { serve } from "../server/serve";
import { sendUnfinishedDistros } from "../modules/reminders/sendUnfinsihedDistros";
import { getNewsFeed } from "../utils/getNewsFeed";
/**
@ -29,13 +30,13 @@ export const onReady = async (bot: ExtendedClient) => {
scheduleJob("0 9 * * 1,3,5", async () => {
await sendUnclaimedArt(bot);
await sendUnclaimedDistros(bot);
});
scheduleJob("0 9 * * 6", async () => {
await sendUnfinishedArt(bot);
await sendUnfinishedDistros(bot);
});
await serve(bot);
} catch (err) {
await bot.debug.send(`Error on ready event: ${(err as Error).message}`);
}

View File

@ -1,6 +1,5 @@
import { execSync } from "child_process";
import { PrismaClient } from "@prisma/client";
import { Client, Events, GatewayIntentBits, WebhookClient } from "discord.js";
import { onInteractionCreate } from "./events/onInteractionCreate";
@ -32,14 +31,12 @@ import { logHandler } from "./utils/logHandler";
GatewayIntentBits.GuildMembers,
],
}) as ExtendedClient;
bot.db = new PrismaClient();
bot.debug = new WebhookClient({ url: process.env.DEBUG });
bot.comm = new WebhookClient({ url: process.env.COMM });
bot.dist = new WebhookClient({ url: process.env.DIST });
bot.news = new WebhookClient({ url: process.env.NEWS });
bot.ticket = new WebhookClient({ url: process.env.TICKET });
bot.ticketLogs = {};
await bot.db.$connect();
const commit = execSync("git rev-parse HEAD").toString().trim();

View File

@ -1,8 +1,6 @@
import { PrismaClient } from "@prisma/client";
import { Client, WebhookClient } from "discord.js";
export interface ExtendedClient extends Client {
db: PrismaClient;
debug: WebhookClient;
comm: WebhookClient;
dist: WebhookClient;

View File

@ -3,7 +3,6 @@ import { join } from "path";
import { AttachmentBuilder } from "discord.js";
import { ActionsToLabel, PlatformsToLabel, Trello } from "../config/Trello";
import { AirtableResponse } from "../interface/AirtableResponse";
import { ExtendedClient } from "../interface/ExtendedClient";
@ -44,12 +43,9 @@ export const checkAirtableRecords = async (bot: ExtendedClient) => {
const {
Name: name,
Reference: images,
Action: action,
"Contact Method": platform,
Handle: handle,
"Anything Else?": note,
"Email Address": email,
"What would you like us to draw?": request,
} = record.fields;
const files: AttachmentBuilder[] = [];
for (const imageUrl of images) {
@ -61,117 +57,10 @@ export const checkAirtableRecords = async (bot: ExtendedClient) => {
});
files.push(file);
}
if (!process.env.TRELLO_TOKEN || !process.env.TRELLO_KEY) {
await bot.debug.send({
content:
"Cannot create card on Trello. Missing environment variables.",
});
await bot.comm.send({
content: `${name} | Trello failed to generate. | References attached below:`,
files,
});
return;
}
const cardRes = await fetch(
`https://api.trello.com/1/cards?idList=${Trello.newCardListId}&key=${
process.env.TRELLO_KEY
}&token=${process.env.TRELLO_TOKEN}&name=${encodeURIComponent(
name
)}&desc=${encodeURIComponent(
`${name} helped us with: ${action}\n\nPlease contact them at ${
platform === "Email" ? email : `${handle} on ${platform}`
} to update them on art.\n\nUSER NOTE: ${note}\nART REQUEST: ${request}`
)}`,
{
method: "POST",
headers: {
Accept: "application/json",
},
}
);
const card = (await cardRes.json()) as { id: string; url: string };
await fetch(
`https://api.trello.com/1/cards/${card.id}/attachments?key=${
process.env.TRELLO_KEY
}&token=${
process.env.TRELLO_TOKEN
}&name=reference.png&url=${encodeURIComponent(
images[0]?.url ?? "https://cdn.naomi.lgbt/banner.png"
)}&setCover=true`,
{
method: "POST",
headers: {
accept: "application/json",
},
}
);
for (const imageUrl of images.slice(1)) {
await fetch(
`https://api.trello.com/1/cards/${card.id}/attachments?key=${
process.env.TRELLO_KEY
}&token=${
process.env.TRELLO_TOKEN
}&name=reference.png&url=${encodeURIComponent(imageUrl.url)}`,
{
method: "POST",
headers: {
accept: "application/json",
},
}
);
}
// Use this if we want to add checklist to every card :3
// await fetch(
// `https://api.trello.com/1/cards/${
// card.id
// }/checklists?idChecklistSource=${
// Trello.checklistId
// }&name=${encodeURIComponent("To Do")}&key=${
// process.env.TRELLO_KEY
// }&token=${process.env.TRELLO_TOKEN}`,
// {
// method: "POST",
// headers: {
// accept: "application/json",
// },
// }
// );
if (PlatformsToLabel[platform]) {
await fetch(
`https://api.trello.com/1/cards/${card.id}/idLabels?value=${PlatformsToLabel[platform]}&key=${process.env.TRELLO_KEY}&token=${process.env.TRELLO_TOKEN}`,
{
method: "POST",
headers: {
accept: "application/json",
},
}
);
}
if (ActionsToLabel[action]) {
await fetch(
`https://api.trello.com/1/cards/${card.id}/idLabels?value=${ActionsToLabel[action]}&key=${process.env.TRELLO_KEY}&token=${process.env.TRELLO_TOKEN}`,
{
method: "POST",
headers: {
accept: "application/json",
},
}
);
}
const msg = await bot.comm.send({
content: `${name} | [Trello](<${card.url}>) | References attached below.\nYou can ignore this ID it's just for the bot: ${card.id}`,
await bot.comm.send({
content: `**FOR:** ${name} - Contact via ${platform}: ${handle}\n\n${note}\n\nReferences:`,
files,
});
await bot.db.rewards.create({
data: {
trelloId: card.id,
createdAt: Date.now(),
claimedBy: "",
completed: false,
messageId: msg.id,
},
});
return;
}
} catch (err) {

View File

@ -1,3 +1,5 @@
import { TextChannel } from "discord.js";
import { ExtendedClient } from "../../interface/ExtendedClient";
/**
@ -7,16 +9,19 @@ import { ExtendedClient } from "../../interface/ExtendedClient";
*/
export const sendUnclaimedArt = async (bot: ExtendedClient) => {
try {
const unclaimed = await bot.db.rewards.findMany({
where: {
claimedBy: "",
},
const guild = await bot.guilds.fetch("1172566005311090798");
const artChannel = (await guild.channels.fetch(
"1172568787330019340"
)) as TextChannel;
const messages = await artChannel.messages.fetch({
after: "1241326793294610443",
});
if (!unclaimed.length) {
const filtered = messages.filter((m) => !m.hasThread);
if (!filtered.size) {
return;
}
const guild = await bot.guilds.fetch("1172566005311090798");
const channel = await guild.channels.fetch("1172568865218252801");
if (!channel || !("send" in channel)) {
@ -28,12 +33,7 @@ export const sendUnclaimedArt = async (bot: ExtendedClient) => {
The following art rewards have not been claimed yet. If you have the capacity to do so, please consider taking one on.
${unclaimed
.map(
(r) =>
`- https://discord.com/channels/1172566005311090798/1172568787330019340/${r.messageId}`
)
.join("\n")}`);
${filtered.map((r) => `- ${r.url}`).join("\n")}`);
} catch (err) {
await bot.debug.send(`Cannot send unclaimed art reminder: ${err}`);
}

View File

@ -0,0 +1,40 @@
import { TextChannel } from "discord.js";
import { ExtendedClient } from "../../interface/ExtendedClient";
/**
* Sends a reminder to claim the unclaimed distribution requests.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const sendUnclaimedDistros = async (bot: ExtendedClient) => {
try {
const guild = await bot.guilds.fetch("1172566005311090798");
const artChannel = (await guild.channels.fetch(
"1173061747737903315"
)) as TextChannel;
const messages = await artChannel.messages.fetch({
after: "1241055357112025168",
});
const filtered = messages.filter((m) => !m.hasThread);
if (!filtered.size) {
return;
}
const channel = await guild.channels.fetch("1173061809834565764");
if (!channel || !("send" in channel)) {
await bot.debug.send(`Failed to find <#1173061809834565764>~!`);
return;
}
await channel.send(`## Unclaimed Distribtions!
The following art rewards have not been distributed yet. If you have the capacity to do so, please consider taking one on.
${filtered.map((r) => `- ${r.url}`).join("\n")}`);
} catch (err) {
await bot.debug.send(`Cannot send unclaimed distro reminder: ${err}`);
}
};

View File

@ -1,3 +1,5 @@
import { TextChannel } from "discord.js";
import { ExtendedClient } from "../../interface/ExtendedClient";
/**
@ -7,36 +9,16 @@ import { ExtendedClient } from "../../interface/ExtendedClient";
*/
export const sendUnfinishedArt = async (bot: ExtendedClient) => {
try {
const unfinished = await bot.db.rewards.findMany({
where: {
claimedBy: {
not: "",
},
completed: false,
},
});
if (!unfinished.length) {
return;
}
const guild = await bot.guilds.fetch("1172566005311090798");
const channel = await guild.channels.fetch("1172568865218252801");
if (!channel || !("send" in channel)) {
await bot.debug.send(`Failed to find <#1172568865218252801>~!`);
return;
const channel = (await guild.channels.fetch(
"1172568787330019340"
)) as TextChannel;
const threads = await channel.threads.fetchActive();
for (const thread of threads.threads.values()) {
await thread.send({
content: `<@!${thread.name}>, this art appears unfinished. When it's complete, please upload the image here and the thread will be processed.`,
});
}
await channel.send(`## Unfinished Art!
The following art rewards have not been completed yet. We would like to keep a turnaround time of a couple of weeks, but your health is also important! So if you have claimed one of these and don't have the capacity to finish it, let Angel or Naomi know.
${unfinished
.map(
(r) =>
`- https://discord.com/channels/1172566005311090798/1172568787330019340/${r.messageId}`
)
.join("\n")}`);
} catch (err) {
await bot.debug.send(`Cannot send unfinished art reminder: ${err}`);
}

View File

@ -0,0 +1,25 @@
import { TextChannel } from "discord.js";
import { ExtendedClient } from "../../interface/ExtendedClient";
/**
* Sends a reminder to finish claimed distribution requests.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const sendUnfinishedDistros = async (bot: ExtendedClient) => {
try {
const guild = await bot.guilds.fetch("1172566005311090798");
const channel = (await guild.channels.fetch(
"1173061747737903315"
)) as TextChannel;
const threads = await channel.threads.fetchActive();
for (const thread of threads.threads.values()) {
await thread.send({
content: `<@!${thread.name}>, this distribution appears unconfirmed. When it's complete, please upload a confirmation image here and the thread will be processed.`,
});
}
} catch (err) {
await bot.debug.send(`Cannot send unfinished art reminder: ${err}`);
}
};

View File

@ -11,6 +11,8 @@ import { ExtendedClient } from "../interface/ExtendedClient";
/**
* Instantiates the web server for listening to webhook payloads.
*
* TODO: Delete this entirely once all trello things are done.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const serve = async (bot: ExtendedClient) => {
@ -80,14 +82,6 @@ export const serve = async (bot: ExtendedClient) => {
await bot.dist.send({
content: `${card.name} | [Trello Card](<${card.url}>) | ${contact}\nYou can ignore this ID it's just for the bot: ${card.id}`,
});
await bot.db.rewards.update({
where: {
trelloId: card.id,
},
data: {
completed: true,
},
});
});
const httpServer = http.createServer(app);