Skip to content

Commit

Permalink
Merge pull request #16 from aleccaputo/features/dink-integration
Browse files Browse the repository at this point in the history
Features/dink integration
  • Loading branch information
aleccaputo authored Nov 26, 2024
2 parents 4eb6a9c + 70f428f commit d8d2e44
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 63 deletions.
8 changes: 8 additions & 0 deletions exceptions/ItemNotFoundException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class ItemNotFoundException extends Error {
constructor(m: string) {
super(m);

// Set the prototype explicitly.
Object.setPrototypeOf(this, ItemNotFoundException.prototype);
}
}
98 changes: 35 additions & 63 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import { ApplicationQuestions } from './services/constants/application-questions
import { createApplicationChannel, sendQuestions } from './services/ApplicationService';
import { formatDiscordUserTag, parseServerCommand } from './services/MessageHelpers';
import { connect } from './services/DataService';
import { extractMessageInformationAndProcessPoints, PointsAction, reactWithBasePoints } from './services/DropSubmissionService';
import {
extractMessageInformationAndProcessPoints,
PointsAction,
processDinkPost,
processNonembedDinkPost,
reactWithBasePoints
} from './services/DropSubmissionService';
import path from 'path';
import fs from 'fs';
import { fetchPointsData } from './services/GoogleApiService';
import { getUser, modifyNicknamePoints, modifyPoints } from './services/UserService';
import { PointType } from './models/PointAudit';
import { schedule } from 'node-cron';
import { ItemNotFoundException } from './exceptions/ItemNotFoundException';

dotenv.config();

Expand Down Expand Up @@ -68,12 +73,13 @@ dotenv.config();
schedulePointsSheetRefresh();

const pointsSheetLookup: Record<string, string> = Object.fromEntries(pointsSheet ?? []);

// console.log(pointsSheetLookup);

await client.login(process.env.TOKEN);
await connect();

client.once('ready', async () => {
// schedule cronjobs
const server = client.guilds.cache.find((guild) => guild.id === serverId);
await server?.members.fetch();
console.log('ready');
Expand Down Expand Up @@ -108,7 +114,7 @@ dotenv.config();
return;
}

// handle forwarding drop submissions to private channel
// handle forwarding drop submissions to private channel for manual point assignment
if (
(message.channel.id === process.env.PUBLIC_SUBMISSIONS_CHANNEL_ID ||
message.channel.id === process.env.HIGH_VALUE_PUBLIC_SUBMISSIONS_CHANNEL_ID) &&
Expand All @@ -127,69 +133,31 @@ dotenv.config();
message.channel.id === process.env.DISCORD_WEBHOOK_DROPS_CHANNEL_ID &&
message.author.id === process.env.DISCORD_WEBHOOK_DROPS_USER_ID
) {
// TODO abstract this to a service
// handle webhook automated points
if (message.embeds[0]) {
const debugChannel = client.channels.cache.get(process.env.RARE_DROP_DEBUG_DUMP_CHANNEL_ID ?? '');
const embed = message.embeds[0];
const embedDescription = embed.description;
const item =
embedDescription?.trim() == 'Just got a pet.' ||
embedDescription?.trim() == "Would've gotten a pet, but already has it."
? 'pet'
: embed.description?.match(/\[(.*?)\]/);
const user = embed.author?.name;
if (user) {
// check that it matches the nickname scheme in the server to only ever match one ie Nickname [
const allUsers = await message?.guild?.members.fetch();
const possibleUser = allUsers?.find((x) =>
(x.nickname ?? '').toLocaleLowerCase().startsWith(`${user.toLocaleLowerCase()}`)
);
if (item && possibleUser) {
if (message.channel.type === ChannelType.GuildText) {
const strippedMatchItemName = item === 'pet' ? item : item[0].replace(/\[|\]/g, '').toLocaleLowerCase();
// fuzzy match
// TODO idk if this is right
// const foundItemPointValue = pointsEntries.find(([key]) => key.includes(strippedMatchItemName))?.[1];
const foundItemPointValue = pointsSheetLookup[strippedMatchItemName];
if (foundItemPointValue) {
const dbUser = await getUser(possibleUser.id);
const newPoints = await modifyPoints(
dbUser,
parseInt(foundItemPointValue, 10),
PointsAction.ADD,
message.author.id,
PointType.AUTOMATED,
message.id
);
if (newPoints) {
await modifyNicknamePoints(newPoints, possibleUser);
await message.channel.send(
`${strippedMatchItemName} is ${foundItemPointValue} points. <@${possibleUser.id}> now has ${newPoints} points.`
);
}
} else {
console.info(`No item matching ${strippedMatchItemName}`);
if (debugChannel && debugChannel?.type === ChannelType.GuildText) {
await debugChannel.send(
`No item matching ${strippedMatchItemName}. Points not given to ${formatDiscordUserTag(
possibleUser.id
)}. Please manually check ${message.url}`
);
}
}
}
} else {
if (debugChannel && debugChannel?.type === ChannelType.GuildText) {
await debugChannel.send(`No user found in discord matching in game name: ${user}.`);
try {
await processDinkPost(message, pointsSheetLookup);
} catch (e) {
if (e instanceof ItemNotFoundException) {
const debugChannel = client.channels.cache.get(process.env.RARE_DROP_DEBUG_DUMP_CHANNEL_ID ?? '');
if (debugChannel && debugChannel.type === ChannelType.GuildText) {
await debugChannel.send(e.message);
}
}
} else {
console.error('no user found on embed');
}
} else {
console.log(message.content);
console.log(`No message content for embed: ${message}`);
try {
await processNonembedDinkPost(message, pointsSheetLookup);
} catch (e) {
if (e instanceof ItemNotFoundException) {
const debugChannel = client.channels.cache.get(process.env.RARE_DROP_DEBUG_DUMP_CHANNEL_ID ?? '');
if (debugChannel && debugChannel.type === ChannelType.GuildText) {
await debugChannel.send(e.message);
}
}
}
}
// handle clan application in private channel
} else {
// TODO, can i make this a slash command
if (message.channel.type === ChannelType.GuildText && message.channel.topic === 'application') {
Expand All @@ -215,8 +183,8 @@ dotenv.config();
if (user.id === client.user?.id) {
return;
}
// handle emoji reactions for manual point processing
if (reaction.message.channel.id === process.env.PRIVATE_SUBMISSIONS_CHANNEL_ID) {
console.log('am am about to process');
const server = client.guilds.cache.find((guild) => guild.id === serverId);
await extractMessageInformationAndProcessPoints(
reaction,
Expand All @@ -228,6 +196,7 @@ dotenv.config();
);
console.log('am am about to process');
}
// handle creating the private application message when new users click the check in the welcome channel
if (reaction.message.channel.id === process.env.INTRO_CHANNEL_ID) {
const emoji = '✅';
if (reaction.emoji.name === emoji) {
Expand All @@ -254,6 +223,7 @@ dotenv.config();
if (user.id === client.user?.id) {
return;
}
// if a reaction is removed for manual points, make sure to subtract those points from the user
if (reaction.message.channel.id === process.env.PRIVATE_SUBMISSIONS_CHANNEL_ID) {
const server = client.guilds.cache.find((guild) => guild.id === serverId);
await extractMessageInformationAndProcessPoints(
Expand All @@ -267,6 +237,7 @@ dotenv.config();

client.on('guildMemberRemove', async (member) => {
if (process.env.REPORTING_CHANNEL_ID) {
// inform moderators when a member leaves the server and when their nickname in the server was
const discordUser = await client.users.fetch(member.id);
const reportingChannel = client.channels.cache.get(process.env.REPORTING_CHANNEL_ID);
if (reportingChannel && reportingChannel.type === ChannelType.GuildText) {
Expand All @@ -284,6 +255,7 @@ dotenv.config();
}
});

// handle all slash commands (logic in ./commands/
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
Expand Down
175 changes: 175 additions & 0 deletions services/DropSubmissionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Channel, ChannelType, Emoji, Guild, Message, MessageReaction, PartialMe
import { getUser, modifyNicknamePoints, modifyPoints } from './UserService';
import { NicknameLengthException } from '../exceptions/NicknameLengthException';
import { PointType } from '../models/PointAudit';
import { formatDiscordUserTag } from './MessageHelpers';
import { ItemNotFoundException } from '../exceptions/ItemNotFoundException';

const NumberEmojis = {
ONE: '1️⃣',
Expand Down Expand Up @@ -145,3 +147,176 @@ export const reactWithBasePoints = async (message: Message) => {
// await message.react(NumberEmojis.NINE);
await message.react(NumberEmojis.TEN);
};

export const processNonembedDinkPost = async (message: Message, pointsSheetLookup: Record<string, string>) => {
// Handle submissions that are not embeded
console.debug(message.content);

if (message.channel.type !== ChannelType.GuildText) {
console.error("The message was not posted in the correct channel: ", message.channel.type);
return;
}

const matches = message.content.match(/\*\*([\s\S]*?)\*\*/g);
console.debug("Matches: ", matches);

if (matches === null) {
console.error("No bold text found in the message content: ", message.content);
return;
}

const pieces = matches.map(part => part.replace(/\*/g, '')).filter(part => (part.length > 0 && !part.includes('*')));
console.debug("Pieces: ", pieces);

if (pieces.length === 1 && message.content.includes("** just got a pet!")) {
// Add "1 x " & " ()" so it matches the expected input later
pieces[1] = "1 x Pet ()";
// Could add a lookup here to get the source of the pet
// The "Pet" section of dink does not have the %SOURCE% parameter available
pieces[2] = "unknown";
} else if (pieces.length !== 3) {
console.error("The message did not return 3 pieces: ", pieces);
return;
}

const player = pieces[0];
const loot = pieces[1].split("\n").map(piece => ({
'item': piece.substring(piece.indexOf(" ", 2) + 1, piece.lastIndexOf(" ")),
'quantity': parseInt(piece.substring(0, piece.indexOf(" ", 1)))
}));

const allUsers = await message?.guild?.members.fetch();
const possibleUser = allUsers?.find((x) => (x.nickname ?? '').toLocaleLowerCase().startsWith(player.toLocaleLowerCase()));

if (!possibleUser) {
console.error("No possible user found: ", possibleUser);
return;
}

if (!loot.length) {
console.error("No loot found: ", loot);
return;
}

const validLoot = loot
.filter(x => x.item.toLocaleLowerCase() in pointsSheetLookup)
.map((itemWorthPoints): {name: string; points: number; quantity: number} => ({
name: itemWorthPoints.item,
points: parseInt(pointsSheetLookup[itemWorthPoints.item.toLocaleLowerCase()], 10),
quantity: itemWorthPoints.quantity
}));

const totalPointsToAdd = validLoot.reduce((total, item) => total + (item.points * item.quantity), 0);

if (validLoot.length === 0) {
console.error("No items are worth points: ", validLoot);
return;
}

const db_user = await getUser(possibleUser.id);

const new_points = await modifyPoints(
db_user,
totalPointsToAdd,
PointsAction.ADD,
message.author.id,
PointType.AUTOMATED,
message.id
);

if (new_points === null) {
console.error("Could not modify points: ", validLoot, db_user, new_points);
return;
}

await modifyNicknamePoints(new_points, possibleUser);

let formattedConfirmationString = '';
validLoot.forEach((x) => {
formattedConfirmationString += `**${x.quantity} x ${x.name}** is **${x.points * x.quantity} points**. <@${possibleUser.id}> now has **${new_points} points**\n`;
});

await message.channel.send(
formattedConfirmationString ||
`new points: ${new_points}, but for some reason i can't tell you the formatted string...`
);
}

/**
* picks apart an embed from the Dink bot, looks up the number of points, and then gives them to the user
* @param message the Discord.Message that was sent and contains the embed
* @param pointsSheetLookup A Record<string, string> containing {itemName: pointValue}
*/
export const processDinkPost = async (message: Message, pointsSheetLookup: Record<string, string>) => {
const embed = message.embeds[0];
const embedDescription = embed.description;
console.log(embed);
const item =
embedDescription?.trim().includes("has a funny feeling like they're being followed") ||
embedDescription?.trim() == "Would've gotten a pet, but already has it."
? 'pet'
: embed.description
?.match(/\[(.*?)\]/g)
?.map((str) => str.slice(1, -1))
.slice(0, -1); // removes the []. splice removes the item source
const user = embed.author?.name;
if (user) {
// check that it matches the nickname scheme in the server to only ever match one ie Nickname [
const allUsers = await message?.guild?.members.fetch();
const possibleUser = allUsers?.find((x) => (x.nickname ?? '').toLocaleLowerCase().startsWith(`${user.toLocaleLowerCase()}`));
// if i have an item and i found a user in the discord server
if (item && possibleUser) {
if (message.channel.type === ChannelType.GuildText) {
const allItemsStripped = item === 'pet' ? [item] : item.map((x) => x.replace(/\[|\]/g, '').toLocaleLowerCase());
let foundLookup: Array<{ name: string; points: string }> = [];
for (const itemName of allItemsStripped) {
const foundItem = pointsSheetLookup[itemName];
if (foundItem) {
foundLookup.push({
name: itemName,
points: foundItem
});
}
}

// i found an item, now modify the backing user points
if (foundLookup.length) {
const dbUser = await getUser(possibleUser.id);
const totalPoints = foundLookup.reduce((acc, cur) => acc + parseInt(cur.points), 0);
const newPoints = await modifyPoints(
dbUser,
totalPoints,
PointsAction.ADD,
message.author.id,
PointType.AUTOMATED,
message.id
);
if (newPoints) {
await modifyNicknamePoints(newPoints, possibleUser);
let formattedConfirmationString = '';
foundLookup.forEach((x) => {
formattedConfirmationString += `${x.name} is ${x.points} points. <@${possibleUser.id}> now has ${newPoints} points.\n`;
});
await message.channel.send(
formattedConfirmationString ||
`new points: ${newPoints}, but for some reason i can't tell you the formatted string...`
);
}
} else {
console.info(`No item matching ${allItemsStripped.toString()}`);
throw new ItemNotFoundException(
`No item matching ${allItemsStripped.toString()}. Points not given to ${formatDiscordUserTag(
possibleUser.id
)}. Please manually check ${message.url}`
);
}
}
} else {
console.log(`No user found in discord matching in game name: ${user}.`);
throw new ItemNotFoundException(`No user found in discord matching in game name: ${user}.`);
}
} else {
console.error('no user found on embed');
throw new ItemNotFoundException('no user found on embed');
}
};

0 comments on commit d8d2e44

Please sign in to comment.