generated from nhcarrigan/template
Compare commits
No commits in common. "c05a33e8d2e98180b579b80678fc47ea12c1c9e9" and "7db3201aefc1dc8675de4e8ca590b0b8eb7c41b4" have entirely different histories.
c05a33e8d2
...
7db3201aef
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@nhcarrigan",
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["src/modules/buttons/*.ts"],
|
|
||||||
"rules": {
|
|
||||||
"jsdoc/require-param": "off",
|
|
||||||
"jsdoc/require-returns": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
/node_modules/
|
|
||||||
/prod/
|
|
||||||
.env
|
|
||||||
/logs/*.txt
|
|
||||||
latestAirtableRecord.txt
|
|
@ -1 +0,0 @@
|
|||||||
"@nhcarrigan/prettier-config"
|
|
18
README.md
18
README.md
@ -1,10 +1,20 @@
|
|||||||
# Art for Palestine Bot
|
# New Repository Template
|
||||||
|
|
||||||
This is a bot for the [Art for Palestine](https://art4palestine.org) charity event. It serves as a bridge between AirTable, Trello, and Discord.
|
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository.
|
||||||
|
|
||||||
|
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
|
||||||
|
|
||||||
|
## Readme
|
||||||
|
|
||||||
|
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
|
||||||
|
|
||||||
|
<!-- # Project Name
|
||||||
|
|
||||||
|
Project Description
|
||||||
|
|
||||||
## Live Version
|
## Live Version
|
||||||
|
|
||||||
This page is currently deployed. [View the live website.](https://discord.gg/kHNyb6Vyf8)
|
This page is currently deployed. [View the live website.]
|
||||||
|
|
||||||
## Feedback and Bugs
|
## Feedback and Bugs
|
||||||
|
|
||||||
@ -26,4 +36,4 @@ Copyright held by Naomi Carrigan.
|
|||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.
|
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. -->
|
||||||
|
@ -1 +0,0 @@
|
|||||||
We need this for the ticket system.
|
|
58
package.json
58
package.json
@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "nodejs-typescript-template",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"description": "A template for my nodejs projects",
|
|
||||||
"main": "prod/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"lint": "eslint src test --max-warnings 0 && prettier src test --check",
|
|
||||||
"start": "node -r dotenv/config prod/index.js",
|
|
||||||
"test": "ts-mocha -u tdd test/**/*.spec.ts --recursive --exit --timeout 10000"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+ssh://git@github.com/naomi-lgbt/nodejs-typescript-template.git"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "22",
|
|
||||||
"pnpm": "9"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"template",
|
|
||||||
"typescript",
|
|
||||||
"eslint",
|
|
||||||
"nodejs",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"author": "Naomi Carrigan",
|
|
||||||
"license": "SEE LICENSE IN https://docs.nhcarrigan.com/#/license",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/naomi-lgbt/nodejs-typescript-template/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/naomi-lgbt/nodejs-typescript-template#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@prisma/client": "5.13.0",
|
|
||||||
"discord.js": "14.15.2",
|
|
||||||
"dotenv": "16.4.5",
|
|
||||||
"express": "4.19.2",
|
|
||||||
"node-schedule": "2.1.1",
|
|
||||||
"winston": "3.13.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nhcarrigan/eslint-config": "1.1.3",
|
|
||||||
"@nhcarrigan/prettier-config": "1.0.1",
|
|
||||||
"@nhcarrigan/typescript-config": "1.0.1",
|
|
||||||
"@types/chai": "4.3.16",
|
|
||||||
"@types/express": "4.17.21",
|
|
||||||
"@types/mocha": "10.0.6",
|
|
||||||
"@types/node": "18.19.33",
|
|
||||||
"@types/node-schedule": "2.1.7",
|
|
||||||
"chai": "4.4.1",
|
|
||||||
"eslint": "8.57.0",
|
|
||||||
"mocha": "10.4.0",
|
|
||||||
"prettier": "2.8.8",
|
|
||||||
"prisma": "5.13.0",
|
|
||||||
"ts-mocha": "10.0.0",
|
|
||||||
"typescript": "5.4.5"
|
|
||||||
}
|
|
||||||
}
|
|
3364
pnpm-lock.yaml
generated
3364
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
sample.env
18
sample.env
@ -1,18 +0,0 @@
|
|||||||
# Bot
|
|
||||||
TOKEN=""
|
|
||||||
MONGO_URI=""
|
|
||||||
|
|
||||||
# Hooks
|
|
||||||
DEBUG=""
|
|
||||||
COMM=""
|
|
||||||
DIST=""
|
|
||||||
NEWS=""
|
|
||||||
|
|
||||||
#Env
|
|
||||||
NODE_ENV="development"
|
|
||||||
|
|
||||||
# Trello
|
|
||||||
TRELLO_KEY=""
|
|
||||||
TRELLO_TOKEN=""
|
|
||||||
TRELLO_SECRET=""
|
|
||||||
TRELLO_HOOK_CALLBACK=""
|
|
@ -1 +0,0 @@
|
|||||||
export const TicketSupportRole = "1173582640843063366";
|
|
@ -1,44 +0,0 @@
|
|||||||
import { Webhooks } from "./Webhooks";
|
|
||||||
|
|
||||||
export const Trello = {
|
|
||||||
boardId: "",
|
|
||||||
newCardListId: "6552f24264ac58e98f8b78cd",
|
|
||||||
readyToSendListId: "6552f248af3c5fb5a81e4788",
|
|
||||||
platformLabels: {
|
|
||||||
discord: "6552f2b18af54bcca998abfe",
|
|
||||||
twitter: "6552f2cb9f21432b29106558",
|
|
||||||
email: "6552f2cee1258dfe159d48c8",
|
|
||||||
},
|
|
||||||
actionLabels: {
|
|
||||||
donate: "6552f2d257c42c85759d0199",
|
|
||||||
call: "6552f2d6d0e69a4c17f5f8ab",
|
|
||||||
protest: "6552f2dbfa9f5ac4267b136a",
|
|
||||||
},
|
|
||||||
checklistId: "6552f66d91daf000137ed4d2",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PlatformsToLabel: Record<string, string> = {
|
|
||||||
Email: Trello.platformLabels.email,
|
|
||||||
Discord: Trello.platformLabels.discord,
|
|
||||||
Twitter: Trello.platformLabels.twitter,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ActionsToLabel: Record<string, string> = {
|
|
||||||
"Sent a donation": Trello.actionLabels.donate,
|
|
||||||
"Called my representatives": Trello.actionLabels.call,
|
|
||||||
"Attended a protest": Trello.actionLabels.protest,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grabs the message from Discord, formats it into a Trello comment.
|
|
||||||
*
|
|
||||||
* @param {string} userName The name of the user that triggered the comment.
|
|
||||||
* @returns {string} The formatted text to send.
|
|
||||||
*/
|
|
||||||
export const TrelloComments: {
|
|
||||||
[key in Webhooks]: (userName: string) => string;
|
|
||||||
} = {
|
|
||||||
[Webhooks.NewCommissions]: (userName) => `Artwork claimed by ${userName}.`,
|
|
||||||
[Webhooks.CompleteCommissions]: (userName) =>
|
|
||||||
`Distribution claimed by ${userName}.`,
|
|
||||||
};
|
|
@ -1,19 +0,0 @@
|
|||||||
import { User } from "discord.js";
|
|
||||||
|
|
||||||
export enum Webhooks {
|
|
||||||
NewCommissions = "1172850885571911760",
|
|
||||||
CompleteCommissions = "1173062416041525259",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grabs the user from Discord, formats it based on the webhook ID.
|
|
||||||
*
|
|
||||||
* @param {User} u The user payload from Discord.
|
|
||||||
* @returns {string} The formatted text to send.
|
|
||||||
*/
|
|
||||||
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!`,
|
|
||||||
};
|
|
@ -1,45 +0,0 @@
|
|||||||
import { Interaction } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
import { ticketClaimHandler } from "../modules/buttons/ticketClaim";
|
|
||||||
import { ticketCloseHandler } from "../modules/buttons/ticketClose";
|
|
||||||
import { ticketOpenHandler } from "../modules/buttons/ticketOpen";
|
|
||||||
import { handleTicketModal } from "../modules/modals/handleTicketModal";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the logic for the interaction create event.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {Interaction} interaction The interaction payload from Discord.
|
|
||||||
*/
|
|
||||||
export const onInteractionCreate = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
interaction: Interaction
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (interaction.isModalSubmit()) {
|
|
||||||
if (interaction.customId === "ticket-modal") {
|
|
||||||
await handleTicketModal(bot, interaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interaction.isButton()) {
|
|
||||||
if (!interaction.inCachedGuild()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (interaction.customId === "ticket") {
|
|
||||||
await ticketOpenHandler(bot, interaction);
|
|
||||||
}
|
|
||||||
if (interaction.customId === "claim") {
|
|
||||||
await ticketClaimHandler(bot, interaction);
|
|
||||||
}
|
|
||||||
if (interaction.customId === "close") {
|
|
||||||
await ticketCloseHandler(bot, interaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in interaction create event: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
import { GuildMember } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the member add event from discord. Gives the new member
|
|
||||||
* the `Members` role.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {GuildMember} member The member payload from Discord.
|
|
||||||
*/
|
|
||||||
export const onMemberAdd = async (bot: ExtendedClient, member: GuildMember) => {
|
|
||||||
try {
|
|
||||||
await member.roles.add("1173580547952484352");
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error on member add event: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,172 +0,0 @@
|
|||||||
import { ChannelType, Message, PermissionFlagsBits } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
import { logTicketMessage } from "../modules/logTicketMessage";
|
|
||||||
import { startTicketPost } from "../modules/messages/startTicketPost";
|
|
||||||
import { getMuteDurationUnit } from "../utils/getMuteDurationUnit";
|
|
||||||
import { isValidWebhook } from "../utils/isValidWebhook";
|
|
||||||
import { logHandler } from "../utils/logHandler";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the message create event. Adds reactions to messages from approved
|
|
||||||
* webhooks.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {Message} message The message payload from Discord.
|
|
||||||
*/
|
|
||||||
export const onMessageCreate = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
message: Message
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (!message.author.bot) {
|
|
||||||
if (
|
|
||||||
message.member?.permissions.has(PermissionFlagsBits.ManageGuild) &&
|
|
||||||
message.content === "~tickets"
|
|
||||||
) {
|
|
||||||
await startTicketPost(bot, message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!message.channel.isDMBased() &&
|
|
||||||
message.channel.name.startsWith("ticket-")
|
|
||||||
) {
|
|
||||||
const id = message.channel.id;
|
|
||||||
const cached = bot.ticketLogs[id];
|
|
||||||
if (!cached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await logTicketMessage(bot, message, cached);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
message.member?.permissions.has(PermissionFlagsBits.ModerateMembers)
|
|
||||||
) {
|
|
||||||
if (message.content.startsWith("!mute")) {
|
|
||||||
const [, id, durationString, ...reason] =
|
|
||||||
message.content.split(/\s+/);
|
|
||||||
const target = await message.guild?.members.fetch(id);
|
|
||||||
if (!target) {
|
|
||||||
await message.reply("Target not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const durationNumber = parseInt(
|
|
||||||
durationString.replace(/\w$/, ""),
|
|
||||||
10
|
|
||||||
);
|
|
||||||
const durationUnit = durationString.replace(/^\d+/, "");
|
|
||||||
const conversion = getMuteDurationUnit(durationUnit);
|
|
||||||
if (!conversion) {
|
|
||||||
await message.reply(`Invalid unit ${durationUnit}.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const time = durationNumber * conversion;
|
|
||||||
if (time > 1000 * 60 * 60 * 24 * 28) {
|
|
||||||
await message.reply("Cannot time out user for more than 28 days.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await target.timeout(time, reason.join(" ") || "No reason provided.");
|
|
||||||
await message.reply({ content: "Done" });
|
|
||||||
const logChannel = message.guild?.channels.cache.find(
|
|
||||||
(c) => c.name === "bot-logs"
|
|
||||||
);
|
|
||||||
if (logChannel && "send" in logChannel) {
|
|
||||||
await logChannel?.send({
|
|
||||||
content: `<@!${message.author.id}> muted <@!${target.id}> (${
|
|
||||||
target.id
|
|
||||||
}) - ${durationString} for: ${
|
|
||||||
reason.join(" ") ?? "No reason provided."
|
|
||||||
}`,
|
|
||||||
allowedMentions: {
|
|
||||||
users: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (message.content.startsWith("!unmute")) {
|
|
||||||
const [, id, ...reason] = message.content.split(/\s+/);
|
|
||||||
const target = await message.guild?.members.fetch(id);
|
|
||||||
if (!target) {
|
|
||||||
await message.reply("Target not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await target.timeout(null, reason.join(" ") || "No reason provided.");
|
|
||||||
const logChannel = message.guild?.channels.cache.find(
|
|
||||||
(c) => c.name === "bot-logs"
|
|
||||||
);
|
|
||||||
await message.reply({ content: "Done" });
|
|
||||||
if (logChannel && "send" in logChannel) {
|
|
||||||
await logChannel?.send({
|
|
||||||
content: `<@!${message.author.id}> unmuted <@!${target.id}> (${
|
|
||||||
target.id
|
|
||||||
}) for: ${reason.join(" ") ?? "No reason provided."}`,
|
|
||||||
allowedMentions: {
|
|
||||||
users: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.channel.type === ChannelType.PublicThread &&
|
|
||||||
message.author.id === message.channel.name
|
|
||||||
) {
|
|
||||||
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();
|
|
||||||
if (!starter) {
|
|
||||||
await message.channel.send(
|
|
||||||
`<@!465650873650118659>, couldn't load starter message for ${message.url}. Please port manually~!`
|
|
||||||
);
|
|
||||||
await message.channel.setArchived(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await message.channel.setArchived(true);
|
|
||||||
await bot.dist.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)) {
|
|
||||||
logHandler.log(
|
|
||||||
"info",
|
|
||||||
`Got bot message from ${message.author.id} but is not a valid webhook.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logHandler.log("info", "Processing webhook message.");
|
|
||||||
await message.react("âś…").catch(async (err) => {
|
|
||||||
await bot.debug.send({
|
|
||||||
content: `[Failed to add reaction:](<${message.url}>) ${err.message}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
logHandler.log("info", "Added reaction~!");
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in message create event: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,77 +0,0 @@
|
|||||||
import {
|
|
||||||
MessageReaction,
|
|
||||||
PartialMessageReaction,
|
|
||||||
PartialUser,
|
|
||||||
TextChannel,
|
|
||||||
ThreadAutoArchiveDuration,
|
|
||||||
User,
|
|
||||||
} from "discord.js";
|
|
||||||
|
|
||||||
import { ClaimTexts } from "../config/Webhooks";
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
import { isValidWebhook } from "../utils/isValidWebhook";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the message reaction add. If added to a valid webhook, processes logic to send
|
|
||||||
* DM to the first user who reacted, remove further reactions.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {MessageReaction | PartialMessageReaction} r The reaction payload from Discord.
|
|
||||||
* @param {User | PartialUser} user The user who reacted.
|
|
||||||
*/
|
|
||||||
export const onReactionAdd = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
r: MessageReaction | PartialMessageReaction,
|
|
||||||
user: User | PartialUser
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (user.bot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reaction = await r.fetch().catch(async (err) => {
|
|
||||||
await bot.debug.send({
|
|
||||||
content: `[Failed to fetch reactions:](<${r.message.url}>) ${err.message}`,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
if (!reaction) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const message = await r.message.fetch().catch(async (err) => {
|
|
||||||
await bot.debug.send({
|
|
||||||
content: `[Failed to fetch message:](<${r.message.url}>) ${err.message}`,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
if (!message || !isValidWebhook(message.author.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (reaction.count > 2) {
|
|
||||||
await r.users.remove(user.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 thread = await channel.threads.create({
|
|
||||||
autoArchiveDuration: ThreadAutoArchiveDuration.OneWeek,
|
|
||||||
name: user.id,
|
|
||||||
startMessage: message,
|
|
||||||
});
|
|
||||||
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}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,45 +0,0 @@
|
|||||||
import { scheduleJob } from "node-schedule";
|
|
||||||
|
|
||||||
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 { sendUnfinishedDistros } from "../modules/reminders/sendUnfinsihedDistros";
|
|
||||||
import { getNewsFeed } from "../utils/getNewsFeed";
|
|
||||||
import { serve } from "../server/serve";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the logic when the bot is ready to receive Discord events.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
*/
|
|
||||||
export const onReady = async (bot: ExtendedClient) => {
|
|
||||||
try {
|
|
||||||
await bot.debug.send("Bot is online and ready to help!");
|
|
||||||
// donor channel
|
|
||||||
await fetchMessages(bot, "1172568787330019340");
|
|
||||||
// delivery channel
|
|
||||||
await fetchMessages(bot, "1173061747737903315");
|
|
||||||
await bot.debug.send("Fetching initial news feed...");
|
|
||||||
await getNewsFeed(bot);
|
|
||||||
await bot.debug.send("Fetching news posts every 10 minutes.");
|
|
||||||
setInterval(async () => await getNewsFeed(bot), 1000 * 60 * 10);
|
|
||||||
await bot.debug.send("Fetching new airtable submissions every 60 minutes.");
|
|
||||||
setInterval(async () => await checkAirtableRecords(bot), 1000 * 60 * 60);
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
};
|
|
73
src/index.ts
73
src/index.ts
@ -1,73 +0,0 @@
|
|||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
import { Client, Events, GatewayIntentBits, WebhookClient } from "discord.js";
|
|
||||||
|
|
||||||
import { onInteractionCreate } from "./events/onInteractionCreate";
|
|
||||||
import { onMemberAdd } from "./events/onMemberAdd";
|
|
||||||
import { onMessageCreate } from "./events/onMessageCreate";
|
|
||||||
import { onReactionAdd } from "./events/onReactionAdd";
|
|
||||||
import { onReady } from "./events/onReady";
|
|
||||||
import { ExtendedClient } from "./interface/ExtendedClient";
|
|
||||||
import { logHandler } from "./utils/logHandler";
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
if (
|
|
||||||
!process.env.TOKEN ||
|
|
||||||
!process.env.DEBUG ||
|
|
||||||
!process.env.COMM ||
|
|
||||||
!process.env.DIST ||
|
|
||||||
!process.env.NEWS ||
|
|
||||||
!process.env.TICKET
|
|
||||||
) {
|
|
||||||
logHandler.log("error", "Missing environment variables.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bot = new Client({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMessageReactions,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
GatewayIntentBits.GuildMembers,
|
|
||||||
],
|
|
||||||
}) as ExtendedClient;
|
|
||||||
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 = {};
|
|
||||||
|
|
||||||
const commit = execSync("git rev-parse HEAD").toString().trim();
|
|
||||||
|
|
||||||
await bot.debug.send({
|
|
||||||
content: `Bot is starting up.\nVersion: ${
|
|
||||||
process.env.npm_package_version
|
|
||||||
}\nCommit: [${commit.slice(
|
|
||||||
0,
|
|
||||||
7
|
|
||||||
)}](<https://github.com/nhcarrigan/art-for-palestine-bot/commit/${commit}>)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on(Events.ClientReady, async () => {
|
|
||||||
await onReady(bot);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on(Events.MessageCreate, async (message) => {
|
|
||||||
await onMessageCreate(bot, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on(Events.MessageReactionAdd, async (r, user) => {
|
|
||||||
await onReactionAdd(bot, r, user);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on(Events.GuildMemberAdd, async (member) => {
|
|
||||||
await onMemberAdd(bot, member);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on(Events.InteractionCreate, async (interaction) => {
|
|
||||||
await onInteractionCreate(bot, interaction);
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.login(process.env.TOKEN);
|
|
||||||
})();
|
|
@ -1,71 +0,0 @@
|
|||||||
export interface AirtableResponse {
|
|
||||||
records: {
|
|
||||||
id: string;
|
|
||||||
createdTime: string;
|
|
||||||
fields: {
|
|
||||||
"Anything Else?"?: string;
|
|
||||||
Reference: {
|
|
||||||
id: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
url: string;
|
|
||||||
filename: string;
|
|
||||||
size: number;
|
|
||||||
type: string;
|
|
||||||
thumbnails?: {
|
|
||||||
small: {
|
|
||||||
url: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
large: {
|
|
||||||
url: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
full: {
|
|
||||||
url: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
"Acknowledgement "?: boolean;
|
|
||||||
"Email Address"?: string;
|
|
||||||
"What would you like us to draw?"?: string;
|
|
||||||
Action: string;
|
|
||||||
Name: string;
|
|
||||||
"Contact Method": string;
|
|
||||||
Created: string;
|
|
||||||
"Proof of Donation"?: {
|
|
||||||
id: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
url: string;
|
|
||||||
filename: string;
|
|
||||||
size: number;
|
|
||||||
type: string;
|
|
||||||
thumbnails: {
|
|
||||||
small: {
|
|
||||||
url: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
large: {
|
|
||||||
url: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
full: {
|
|
||||||
url: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
"Donation Amount"?: number;
|
|
||||||
Fund?: string;
|
|
||||||
Handle?: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { ButtonInteraction } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "./ExtendedClient";
|
|
||||||
|
|
||||||
export type ButtonHandler = (
|
|
||||||
Bot: ExtendedClient,
|
|
||||||
interaction: ButtonInteraction<"cached">
|
|
||||||
) => Promise<void>;
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Client, WebhookClient } from "discord.js";
|
|
||||||
|
|
||||||
export interface ExtendedClient extends Client {
|
|
||||||
debug: WebhookClient;
|
|
||||||
comm: WebhookClient;
|
|
||||||
dist: WebhookClient;
|
|
||||||
news: WebhookClient;
|
|
||||||
ticket: WebhookClient;
|
|
||||||
lastArticle: number;
|
|
||||||
ticketLogs: { [key: string]: string };
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonBuilder,
|
|
||||||
ButtonStyle,
|
|
||||||
Embed,
|
|
||||||
GuildMember,
|
|
||||||
Message,
|
|
||||||
} from "discord.js";
|
|
||||||
|
|
||||||
import { TicketSupportRole } from "../../config/Tickets";
|
|
||||||
import { ButtonHandler } from "../../interface/ButtonHandler";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the process of claiming a ticket.
|
|
||||||
*/
|
|
||||||
export const ticketClaimHandler: ButtonHandler = async (bot, interaction) => {
|
|
||||||
try {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const { guild, message, member } = interaction;
|
|
||||||
const { embeds } = message;
|
|
||||||
const supportRole = await guild.roles.fetch(TicketSupportRole);
|
|
||||||
|
|
||||||
if (!supportRole) {
|
|
||||||
await interaction.editReply("Cannot find support role!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSupport = (member as GuildMember).roles.cache.has(supportRole.id);
|
|
||||||
|
|
||||||
if (!isSupport) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: "Only support members can claim a ticket.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticketEmbed = embeds[0] as Embed;
|
|
||||||
const updatedEmbed = {
|
|
||||||
title: ticketEmbed.title || "Lost the Title",
|
|
||||||
description: ticketEmbed.description || "Lost the Description",
|
|
||||||
fields: [{ name: "Claimed by:", value: `<@${member.user.id}>` }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const claimButton = new ButtonBuilder()
|
|
||||||
.setCustomId("claim")
|
|
||||||
.setStyle(ButtonStyle.Success)
|
|
||||||
.setLabel("Claim this ticket!")
|
|
||||||
.setEmoji("âś‹")
|
|
||||||
.setDisabled(true);
|
|
||||||
const closeButton = new ButtonBuilder()
|
|
||||||
.setCustomId("close")
|
|
||||||
.setStyle(ButtonStyle.Danger)
|
|
||||||
.setLabel("Close this ticket!")
|
|
||||||
.setEmoji("🗑️");
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents([
|
|
||||||
claimButton,
|
|
||||||
closeButton,
|
|
||||||
]);
|
|
||||||
|
|
||||||
await (message as Message).edit({
|
|
||||||
embeds: [updatedEmbed],
|
|
||||||
components: [row],
|
|
||||||
});
|
|
||||||
|
|
||||||
await interaction.editReply(
|
|
||||||
"I have marked you as responsible for this query."
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in ticket claim module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
await interaction.editReply({
|
|
||||||
content:
|
|
||||||
"Forgive me, but I failed to complete your request. Please try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,60 +0,0 @@
|
|||||||
import { GuildMember, EmbedBuilder, TextChannel } from "discord.js";
|
|
||||||
|
|
||||||
import { TicketSupportRole } from "../../config/Tickets";
|
|
||||||
import { ButtonHandler } from "../../interface/ButtonHandler";
|
|
||||||
import { generateLogs } from "../generateLogs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles closing a ticket.
|
|
||||||
*/
|
|
||||||
export const ticketCloseHandler: ButtonHandler = async (bot, interaction) => {
|
|
||||||
try {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const { guild, member, channel } = interaction;
|
|
||||||
|
|
||||||
if (!guild || !member || !channel) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: "Error finding the guild!",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportRole = await guild.roles.fetch(TicketSupportRole);
|
|
||||||
|
|
||||||
if (!supportRole) {
|
|
||||||
await interaction.editReply("Cannot find support role!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSupport = (member as GuildMember).roles.cache.has(supportRole.id);
|
|
||||||
|
|
||||||
if (!isSupport) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: "Only support members can close a ticket.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logEmbed = new EmbedBuilder();
|
|
||||||
logEmbed.setTitle("Ticket Closed");
|
|
||||||
logEmbed.setDescription(`Ticket closed by <@!${member.user.id}>`);
|
|
||||||
logEmbed.addFields({
|
|
||||||
name: "User",
|
|
||||||
value:
|
|
||||||
(channel as TextChannel)?.name.split("-").slice(1).join("-") ||
|
|
||||||
"unknown",
|
|
||||||
});
|
|
||||||
|
|
||||||
const logFile = await generateLogs(bot, channel.id);
|
|
||||||
await bot.ticket.send({ embeds: [logEmbed], files: [logFile] });
|
|
||||||
await channel.delete();
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in close ticket module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
await interaction.editReply({
|
|
||||||
content:
|
|
||||||
"Forgive me, but I failed to complete your request. Please try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,41 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionRowBuilder,
|
|
||||||
ModalBuilder,
|
|
||||||
ModalActionRowComponentBuilder,
|
|
||||||
TextInputBuilder,
|
|
||||||
TextInputStyle,
|
|
||||||
} from "discord.js";
|
|
||||||
|
|
||||||
import { ButtonHandler } from "../../interface/ButtonHandler";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the modal for opening a new ticket.
|
|
||||||
*/
|
|
||||||
export const ticketOpenHandler: ButtonHandler = async (bot, interaction) => {
|
|
||||||
try {
|
|
||||||
const ticketModal = new ModalBuilder()
|
|
||||||
.setCustomId("ticket-modal")
|
|
||||||
.setTitle("Ticket System");
|
|
||||||
const reasonInput = new TextInputBuilder()
|
|
||||||
.setCustomId("reason")
|
|
||||||
.setLabel("Why are you opening this ticket?")
|
|
||||||
.setStyle(TextInputStyle.Paragraph)
|
|
||||||
.setRequired(true)
|
|
||||||
.setMaxLength(1000);
|
|
||||||
|
|
||||||
const actionRow =
|
|
||||||
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
|
||||||
reasonInput
|
|
||||||
);
|
|
||||||
ticketModal.addComponents(actionRow);
|
|
||||||
await interaction.showModal(ticketModal);
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in open ticket module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
await interaction.editReply({
|
|
||||||
content:
|
|
||||||
"Forgive me, but I failed to complete your request. Please try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,75 +0,0 @@
|
|||||||
import { readFile, writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import { AttachmentBuilder } from "discord.js";
|
|
||||||
|
|
||||||
import { AirtableResponse } from "../interface/AirtableResponse";
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the Airtable records and pipes new records to trello.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
*/
|
|
||||||
export const checkAirtableRecords = async (bot: ExtendedClient) => {
|
|
||||||
try {
|
|
||||||
const latestId = await readFile(
|
|
||||||
join(__dirname, "..", "..", "latestAirtableRecord.txt"),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
const records = await fetch(
|
|
||||||
`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/${process.env.AIRTABLE_TABLE_ID}?maxRecords=100&sort%5B0%5D%5Bfield%5D=Created&sort%5B0%5D%5Bdirection%5D=desc`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${process.env.AIRTABLE_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const res = (await records.json()) as AirtableResponse;
|
|
||||||
const alreadySeenIndex = res.records.findIndex((r) => r.id === latestId);
|
|
||||||
if (alreadySeenIndex === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newRecords = res.records.slice(0, alreadySeenIndex);
|
|
||||||
const newLatestId = newRecords[0]?.id as string;
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
join(__dirname, "..", "..", "latestAirtableRecord.txt"),
|
|
||||||
newLatestId,
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const record of newRecords) {
|
|
||||||
const {
|
|
||||||
Name: name,
|
|
||||||
Reference: images,
|
|
||||||
"Contact Method": platform,
|
|
||||||
Handle: handle,
|
|
||||||
"What would you like us to draw?": note,
|
|
||||||
"Anything Else?": additional,
|
|
||||||
} = record.fields;
|
|
||||||
const files: AttachmentBuilder[] = [];
|
|
||||||
for (const imageUrl of images) {
|
|
||||||
const image = await fetch(imageUrl.url);
|
|
||||||
const imageBuffer = await image.arrayBuffer();
|
|
||||||
const imageFinal = Buffer.from(imageBuffer);
|
|
||||||
const file = new AttachmentBuilder(imageFinal, {
|
|
||||||
name: "reference.png",
|
|
||||||
});
|
|
||||||
files.push(file);
|
|
||||||
}
|
|
||||||
await bot.comm.send({
|
|
||||||
content: `**FOR:** ${name} - Contact via ${platform}: ${handle}\n\n${note?.slice(
|
|
||||||
0,
|
|
||||||
1000
|
|
||||||
)}\n\n${additional?.slice(0, 1000)}\n\nReferences:`,
|
|
||||||
files,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in check airtable records: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,32 +0,0 @@
|
|||||||
import { writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the initial ticket log file.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {string} channelId The ticket channel ID, used as a unique identifier.
|
|
||||||
* @param {string} user The user tag of the person who opened the ticket.
|
|
||||||
* @param {string} content The initial content of the log file.
|
|
||||||
*/
|
|
||||||
export const createLogFile = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
channelId: string,
|
|
||||||
user: string,
|
|
||||||
content: string
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
bot.ticketLogs[channelId] = channelId;
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
join(process.cwd(), "logs", `${channelId}.txt`),
|
|
||||||
`[${new Date().toLocaleString()}] - **TICKET CREATED**\n[${new Date().toLocaleString()}] - ${user}: ${content}\n`
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in create log file module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
import { readFile, unlink } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import { AttachmentBuilder } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To run when a ticket is closed. Finds the ticket log file,
|
|
||||||
* creates a message attachement with the logs, and deletes the file.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {string} channelId The channel ID of the ticket.
|
|
||||||
* @returns {Promise<AttachmentBuilder>} The log file as a Discord attachment.
|
|
||||||
*/
|
|
||||||
export const generateLogs = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
channelId: string
|
|
||||||
): Promise<AttachmentBuilder> => {
|
|
||||||
try {
|
|
||||||
delete bot.ticketLogs[channelId];
|
|
||||||
|
|
||||||
const logs = await readFile(
|
|
||||||
join(process.cwd(), "logs", `${channelId}.txt`),
|
|
||||||
"utf8"
|
|
||||||
).catch(() => "no logs found...");
|
|
||||||
|
|
||||||
const attachment = new AttachmentBuilder(Buffer.from(logs, "utf-8"), {
|
|
||||||
name: "log.txt",
|
|
||||||
});
|
|
||||||
|
|
||||||
await unlink(join(process.cwd(), "logs", `${channelId}.txt`)).catch(
|
|
||||||
() => null
|
|
||||||
);
|
|
||||||
|
|
||||||
return attachment;
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in generate logs module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
return new AttachmentBuilder(
|
|
||||||
Buffer.from("An error occurred fetching these logs.", "utf-8"),
|
|
||||||
{ name: "log.txt" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,39 +0,0 @@
|
|||||||
import { readFile, writeFile } from "fs/promises";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
import { Message } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs messages to a file to track ticket activity.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {Message} message The Discord message payload.
|
|
||||||
* @param {string} logId The logId to identify the file.
|
|
||||||
*/
|
|
||||||
export const logTicketMessage = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
message: Message,
|
|
||||||
logId: string
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const logFile = await readFile(
|
|
||||||
join(process.cwd(), "logs", `${logId}.txt`),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsedString = `[${new Date(
|
|
||||||
message.createdTimestamp
|
|
||||||
).toLocaleString()}] - ${message.author.tag}: ${message.content}\n`;
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
join(process.cwd(), "logs", `${logId}.txt`),
|
|
||||||
logFile + parsedString
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in log ticket message module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,40 +0,0 @@
|
|||||||
import { ExtendedClient } from "../../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Caches the messages from a specific channel. This should be done to ensure
|
|
||||||
* all donation + delivery messages are cached and reaction payloads will be received.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {string} channelId The ID of the channel to cache.
|
|
||||||
*/
|
|
||||||
export const fetchMessages = async (bot: ExtendedClient, channelId: string) => {
|
|
||||||
try {
|
|
||||||
await bot.debug.send(`Caching messages for ${channelId}`);
|
|
||||||
const guild = await bot.guilds.fetch("1172566005311090798");
|
|
||||||
const channel = await guild.channels.fetch(channelId);
|
|
||||||
|
|
||||||
if (!channel || !("send" in channel)) {
|
|
||||||
await bot.debug.send(`Failed to load <#${channelId}>~!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let before: string | undefined = undefined;
|
|
||||||
|
|
||||||
await bot.debug.send("Fetching first wave of messages.");
|
|
||||||
let messages = await channel.messages.fetch({ limit: 100 });
|
|
||||||
|
|
||||||
while (messages.size === 100) {
|
|
||||||
before = messages.last()?.id;
|
|
||||||
if (!before) {
|
|
||||||
await bot.debug.send("Failed to get oldest messages from batch.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await bot.debug.send(
|
|
||||||
`Found more than 100 messages. Fetching next batch from ${before}`
|
|
||||||
);
|
|
||||||
messages = await channel.messages.fetch({ limit: 100, before });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send({ content: `` });
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,48 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonBuilder,
|
|
||||||
ButtonStyle,
|
|
||||||
EmbedBuilder,
|
|
||||||
Message,
|
|
||||||
} from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the logic to start a ticket post.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
* @param {Message} message The message payload from Discord.
|
|
||||||
*/
|
|
||||||
export const startTicketPost = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
message: Message
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const embed = new EmbedBuilder();
|
|
||||||
embed.setTitle("Need Help?");
|
|
||||||
embed.setDescription(
|
|
||||||
`If you have concerns with behavior in this Discord server, click the button below to open a private ticket with the Moderation team.
|
|
||||||
|
|
||||||
If you want to contact the team for any other reason, you can message an Admin or send an email to \`contact@art4palestine.org\`.`
|
|
||||||
);
|
|
||||||
embed.setColor("#0099ff");
|
|
||||||
|
|
||||||
const button = new ButtonBuilder()
|
|
||||||
.setLabel("Open a Ticket!")
|
|
||||||
.setEmoji("âť”")
|
|
||||||
.setStyle(ButtonStyle.Primary)
|
|
||||||
.setCustomId("ticket");
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
|
|
||||||
await message.channel.send({ embeds: [embed], components: [row] });
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in start ticket post module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
await message.reply({
|
|
||||||
content:
|
|
||||||
"Forgive me, but I failed to complete your request. Please try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,82 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonBuilder,
|
|
||||||
ButtonStyle,
|
|
||||||
ChannelType,
|
|
||||||
EmbedBuilder,
|
|
||||||
ModalSubmitInteraction,
|
|
||||||
TextChannel,
|
|
||||||
} from "discord.js";
|
|
||||||
|
|
||||||
import { TicketSupportRole } from "../../config/Tickets";
|
|
||||||
import { ExtendedClient } from "../../interface/ExtendedClient";
|
|
||||||
import { createLogFile } from "../createLogsFile";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles responding to the ticket modal.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's discord instance.
|
|
||||||
* @param {ModalSubmitInteraction} interaction The interaction payload from Discord.
|
|
||||||
*/
|
|
||||||
export const handleTicketModal = async (
|
|
||||||
bot: ExtendedClient,
|
|
||||||
interaction: ModalSubmitInteraction
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const { guild, user, channel } = interaction;
|
|
||||||
const reason = interaction.fields.getTextInputValue("reason");
|
|
||||||
|
|
||||||
if (!guild || !channel || !("threads" in channel)) {
|
|
||||||
await interaction.editReply(
|
|
||||||
"Forgive me, but this can only be done within a server"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const claimButton = new ButtonBuilder()
|
|
||||||
.setCustomId("claim")
|
|
||||||
.setStyle(ButtonStyle.Success)
|
|
||||||
.setLabel("Claim this ticket!")
|
|
||||||
.setEmoji("âś‹");
|
|
||||||
const closeButton = new ButtonBuilder()
|
|
||||||
.setCustomId("close")
|
|
||||||
.setStyle(ButtonStyle.Danger)
|
|
||||||
.setLabel("Close this ticket!")
|
|
||||||
.setEmoji("🗑️");
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents([
|
|
||||||
claimButton,
|
|
||||||
closeButton,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const ticketEmbed = new EmbedBuilder();
|
|
||||||
ticketEmbed.setTitle("Ticket Created");
|
|
||||||
ticketEmbed.setDescription(
|
|
||||||
`<@!${user.id}> opened a ticket for:\n${reason}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const ticketThread = await (channel as TextChannel).threads.create({
|
|
||||||
name: `ticket-${user.username}`,
|
|
||||||
type: ChannelType.PrivateThread,
|
|
||||||
});
|
|
||||||
await ticketThread.members.add(user.id);
|
|
||||||
await ticketThread.send({
|
|
||||||
content: `<@&${TicketSupportRole}>, a ticket has been opened!`,
|
|
||||||
embeds: [ticketEmbed],
|
|
||||||
components: [row],
|
|
||||||
});
|
|
||||||
await createLogFile(bot, ticketThread.id, user.tag, reason);
|
|
||||||
await interaction.editReply(
|
|
||||||
"Your ticket channel has been created! Please head there and describe the issue you are having."
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in ticket modal module: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
await interaction.editReply({
|
|
||||||
content:
|
|
||||||
"Forgive me, but I failed to complete your request. Please try again later.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,40 +0,0 @@
|
|||||||
import { TextChannel } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a reminder to claim the unclaimed art rewards.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
*/
|
|
||||||
export const sendUnclaimedArt = async (bot: ExtendedClient) => {
|
|
||||||
try {
|
|
||||||
const guild = await bot.guilds.fetch("1172566005311090798");
|
|
||||||
const artChannel = (await guild.channels.fetch(
|
|
||||||
"1172568787330019340"
|
|
||||||
)) as TextChannel;
|
|
||||||
const messages = await artChannel.messages.fetch({
|
|
||||||
after: "1241326793294610443",
|
|
||||||
});
|
|
||||||
const filtered = messages.filter((m) => !m.hasThread);
|
|
||||||
|
|
||||||
if (!filtered.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = await guild.channels.fetch("1172568865218252801");
|
|
||||||
|
|
||||||
if (!channel || !("send" in channel)) {
|
|
||||||
await bot.debug.send(`Failed to find <#1172568865218252801>~!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await channel.send(`## Unclaimed Art!
|
|
||||||
|
|
||||||
The following art rewards have not been claimed 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 art reminder: ${err}`);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,40 +0,0 @@
|
|||||||
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}`);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
import { TextChannel } from "discord.js";
|
|
||||||
|
|
||||||
import { ExtendedClient } from "../../interface/ExtendedClient";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a reminder to finish claimed art rewards.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
*/
|
|
||||||
export const sendUnfinishedArt = async (bot: ExtendedClient) => {
|
|
||||||
try {
|
|
||||||
const guild = await bot.guilds.fetch("1172566005311090798");
|
|
||||||
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.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(`Cannot send unfinished art reminder: ${err}`);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
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}`);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,94 +0,0 @@
|
|||||||
import { createHmac } from "crypto";
|
|
||||||
import { readFile } from "fs/promises";
|
|
||||||
import http from "http";
|
|
||||||
import https from "https";
|
|
||||||
|
|
||||||
import express from "express";
|
|
||||||
|
|
||||||
import { Trello } from "../config/Trello";
|
|
||||||
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) => {
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
app.get("/", (_req, res) => {
|
|
||||||
res.header("Content-Type", "text/html");
|
|
||||||
res.status(200).send(`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Art 4 Palestine Bot</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="description" content="A bot to facilitate our initiative to raise funds for Palestine relief charities by rewarding donors with art." />
|
|
||||||
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>Art 4 Palestine Bot</h1>
|
|
||||||
<section>
|
|
||||||
<p>A bot to facilitate our initiative to raise funds for Palestine relief charities by rewarding donors with art.</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>Links</h2>
|
|
||||||
<p>
|
|
||||||
<a href="https://codeberg.org/nhcarrigan/a4p-bot">
|
|
||||||
<i class="fa-solid fa-code"></i> Source Code
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="https://docs.nhcarrigan.com">
|
|
||||||
<i class="fa-solid fa-book"></i> Documentation
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="https://chat.nhcarrigan.com">
|
|
||||||
<i class="fa-solid fa-circle-info"></i> Support
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</html>`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const httpServer = http.createServer(app);
|
|
||||||
|
|
||||||
httpServer.listen(10080, async () => {
|
|
||||||
await bot.debug.send("http server listening on port 10080");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
const privateKey = await readFile(
|
|
||||||
"/etc/letsencrypt/live/afp.nhcarrigan.com/privkey.pem",
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
const certificate = await readFile(
|
|
||||||
"/etc/letsencrypt/live/afp.nhcarrigan.com/cert.pem",
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
const ca = await readFile(
|
|
||||||
"/etc/letsencrypt/live/afp.nhcarrigan.com/chain.pem",
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
const credentials = {
|
|
||||||
key: privateKey,
|
|
||||||
cert: certificate,
|
|
||||||
ca: ca,
|
|
||||||
};
|
|
||||||
|
|
||||||
const httpsServer = https.createServer(credentials, app);
|
|
||||||
|
|
||||||
httpsServer.listen(10443, async () => {
|
|
||||||
await bot.debug.send("https server listening on port 10443");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Parses a unit string into units.
|
|
||||||
*
|
|
||||||
* @param {string} unit The unit string to parse.
|
|
||||||
* @returns {number} The unit conversion rate, to convert a number to ms.
|
|
||||||
*/
|
|
||||||
export const getMuteDurationUnit = (unit: string): number | null => {
|
|
||||||
switch (unit.toLowerCase()) {
|
|
||||||
case "m":
|
|
||||||
case "minute":
|
|
||||||
case "min":
|
|
||||||
case "minutes":
|
|
||||||
return 1000 * 60;
|
|
||||||
case "h":
|
|
||||||
case "hours":
|
|
||||||
case "hour":
|
|
||||||
return 1000 * 60 * 60;
|
|
||||||
case "d":
|
|
||||||
case "day":
|
|
||||||
case "days":
|
|
||||||
return 1000 * 60 * 60 * 24;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
@ -1,119 +0,0 @@
|
|||||||
import { ExtendedClient } from "../interface/ExtendedClient";
|
|
||||||
|
|
||||||
const getHeadlineArticle = async (bot: ExtendedClient) => {
|
|
||||||
try {
|
|
||||||
const req = await fetch(
|
|
||||||
"https://www.aljazeera.com/tag/israel-palestine-conflict/",
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Wp-site": "Naomi's Art for Palestine Bot",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const res = await req.text();
|
|
||||||
const link = res.match(/<a target="" href="(.*)">Live\supdates<\/a>/);
|
|
||||||
return link?.[1];
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in fetching latest news post: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArticleChildren = async (bot: ExtendedClient, slug: string) => {
|
|
||||||
try {
|
|
||||||
const req = await fetch(
|
|
||||||
`https://www.aljazeera.com/graphql?wp-site=aje&operationName=SingleLiveBlogChildrensQuery&variables=%7B%22postName%22%3A%22${slug}%22%7D&extensions=%7B%7D`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Wp-site": "Naomi's Art for Palestine Bot",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const res = (await req.json()) as {
|
|
||||||
data: {
|
|
||||||
article: {
|
|
||||||
children: number[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return res.data.article.children;
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in fetching article children: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArticleInfo = async (bot: ExtendedClient, postId: number) => {
|
|
||||||
try {
|
|
||||||
const req = await fetch(
|
|
||||||
`https://www.aljazeera.com/graphql?wp-site=aje&operationName=LiveBlogUpdateQuery&variables=%7B%22postID%22%3A${postId}%2C%22postType%22%3A%22liveblog-update%22%2C%22preview%22%3A%22%22%2C%22isAmp%22%3Afalse%7D&extensions=%7B%7D`
|
|
||||||
);
|
|
||||||
const res = (await req.json()) as {
|
|
||||||
data: {
|
|
||||||
posts: {
|
|
||||||
link: string;
|
|
||||||
title: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return res.data.posts;
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in fetching article info: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the GraphQL feed from Aljazeera's Palestine live feed.
|
|
||||||
*
|
|
||||||
* @param {ExtendedClient} bot The bot's Discord instance.
|
|
||||||
*/
|
|
||||||
export const getNewsFeed = async (bot: ExtendedClient) => {
|
|
||||||
try {
|
|
||||||
const headline = await getHeadlineArticle(bot);
|
|
||||||
if (!headline) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const children = await getArticleChildren(
|
|
||||||
bot,
|
|
||||||
headline.split("/").slice(-1).join()
|
|
||||||
);
|
|
||||||
if (!children) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!bot.lastArticle) {
|
|
||||||
bot.lastArticle = children[0];
|
|
||||||
await bot.debug.send(
|
|
||||||
`No cache found. Caching ${children[0]} and skipping run.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const laterChildren = children.slice(0, children.indexOf(bot.lastArticle));
|
|
||||||
bot.lastArticle = children[0];
|
|
||||||
for (const postId of laterChildren) {
|
|
||||||
const article = await getArticleInfo(bot, postId);
|
|
||||||
if (!article) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await bot.news.send(
|
|
||||||
`[${article.title}](<https://www.aljazeera.com${article.link.replace(
|
|
||||||
/\/$/,
|
|
||||||
""
|
|
||||||
)}?update=${article.id}>)\n--------`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
await bot.debug.send(
|
|
||||||
`Error in fetching news feed: ${(err as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Webhooks } from "../config/Webhooks";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the webhook ID is present in the enum.
|
|
||||||
*
|
|
||||||
* @param {string} id The ID to check.
|
|
||||||
* @returns {boolean} If the webhook ID is valid.
|
|
||||||
*/
|
|
||||||
export const isValidWebhook = (id: string): id is Webhooks =>
|
|
||||||
(Object.values(Webhooks) as string[]).includes(id);
|
|
@ -1,24 +0,0 @@
|
|||||||
import { createLogger, format, transports, config } from "winston";
|
|
||||||
|
|
||||||
const { combine, timestamp, colorize, printf } = format;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard log handler, using winston to wrap and format
|
|
||||||
* messages. Call with `logHandler.log(level, message)`.
|
|
||||||
*
|
|
||||||
* @param {string} level - The log level to use.
|
|
||||||
* @param {string} message - The message to log.
|
|
||||||
*/
|
|
||||||
export const logHandler = createLogger({
|
|
||||||
levels: config.npm.levels,
|
|
||||||
level: "silly",
|
|
||||||
transports: [new transports.Console()],
|
|
||||||
format: combine(
|
|
||||||
timestamp({
|
|
||||||
format: "YYYY-MM-DD HH:mm:ss",
|
|
||||||
}),
|
|
||||||
colorize(),
|
|
||||||
printf((info) => `${info.level}: ${[info.timestamp]}: ${info.message}`)
|
|
||||||
),
|
|
||||||
exitOnError: false,
|
|
||||||
});
|
|
@ -1,7 +0,0 @@
|
|||||||
import { assert } from "chai";
|
|
||||||
|
|
||||||
suite("This is an example test", () => {
|
|
||||||
test("It uses the assert API", () => {
|
|
||||||
assert.isTrue(true);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@nhcarrigan/typescript-config",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "./src",
|
|
||||||
"outDir": "./prod"
|
|
||||||
},
|
|
||||||
"exclude": ["./test"]
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user