Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,47 @@ A Discord bot that automatically tracks CS2 players and roasts them based on the

Find your Steam64 ID at [steamid.io](https://steamid.io/)

## DM Notifications

### Automatic DM Roasts

When you link your account with `/link`, the bot will send you a test message to confirm it can DM you. If successful:
- ✅ You'll receive roasts in DMs when new matches are detected
- ✅ You'll also get roasted in server channels (if you're linked there)
- ✅ Immediate notifications even when away from servers

**Note:** Make sure your Discord DMs are open:
- Settings → Privacy & Safety → Allow direct messages from server members

### Opting Out

Don't want DM roasts? Use `/optout dm_roasts` to disable them.
- You'll still get roasted in servers
- Re-run `/link` to re-enable DM notifications

### Admin-Linked Users

If a server admin links your account, you'll only receive DMs if:
1. You have the bot installed to your account, AND
2. Your DMs are open

Otherwise, you'll only be roasted in that server's channels.

### Troubleshooting

**Not receiving DMs?**

1. Check your Discord settings:
- Settings → Privacy & Safety → Allow direct messages from server members
2. Make sure you haven't blocked the bot
3. Re-run `/link steam64_id:YOUR_ID` to test the DM connection
4. If the bot says "DM notifications enabled" but you still don't receive them, check your message requests folder

**Still having issues?**
- DM notifications require the bot to be able to send you messages
- Some users may have privacy settings that prevent bots from DMing them
- You'll still receive roasts in server channels even if DMs don't work

## Commands

### Available Everywhere
Expand All @@ -45,6 +86,7 @@ Find your Steam64 ID at [steamid.io](https://steamid.io/)
- `/roast` - Roast yourself
- `/roast user:@username` - Roast someone else
- `/tracker check` - Manually check for new matches
- `/optout dm_roasts` - Disable DM roast notifications

### Server Only

Expand Down
38 changes: 32 additions & 6 deletions services/matchTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const chatGPTRoastGenerator = require('./chatGPTRoastGenerator');
const config = require('../config');
const { loadUserLinks } = require('../utils/userLinksManager');
const { getGuildConfig } = require('../utils/guildConfigManager');
const { isDMRoastsEnabled } = require('../utils/userPreferencesManager');

const TRACKER_DATA_PATH = path.join(__dirname, '../data/matchTrackerData.json');

Expand Down Expand Up @@ -374,15 +375,10 @@ class MatchTracker {
const userLinks = loadUserLinks();
const userData = userLinks[discordUserId];

if (!userData || !userData.guilds || userData.guilds.length === 0) {
console.log(`User ${discordUserId} has no guild associations`);
return;
}

const playerName = profileData.name || 'Unknown Player';
const currentMatchCount = profileData.total_matches || 0;

// Generate roast once (cached for all guilds) - ensures same roast for same user
// Generate roast once (cached for DM and all guilds) - ensures same roast for same user
const cacheKey = `${discordUserId}-${Date.now()}`;
let selectedRoast;

Expand Down Expand Up @@ -422,8 +418,38 @@ class MatchTracker {
selectedRoast = this.roastCache[cacheKey];
}

// Try to send DM if user has opted in
if (isDMRoastsEnabled(discordUserId)) {
try {
const user = await this.client.users.fetch(discordUserId);
const dmChannel = await user.createDM();

await dmChannel.send({
content: `${selectedRoast}\n-# [Data Provided by Leetify](<https://leetify.com/>) • [Steam Profile](<https://steamcommunity.com/profiles/${steam64Id}>)`,
});

console.log(`[DM] Sent roast to ${playerName} via DM`);
} catch (error) {
// Discord error code 50007 = Cannot send messages to this user (DMs disabled or bot blocked)
if (error.code === 50007) {
console.log(`[DM] User ${playerName} has DMs disabled or blocked bot`);
} else {
console.log(`[DM] Failed to send roast to ${playerName}: ${error.message} (code: ${error.code || 'unknown'})`);
}
// Continue to guild notifications even if DM fails
}
}

// Send roast to each guild where user is linked
let successCount = 0;
const hasGuilds = userData && userData.guilds && userData.guilds.length > 0;

if (!hasGuilds) {
const dmStatus = isDMRoastsEnabled(discordUserId) ? '(DM sent)' : '(no DM enabled)';
console.log(`User ${discordUserId} has no guild associations ${dmStatus}`);
return; // Exit if no guilds (DM was already sent above if enabled)
}

for (const guildId of userData.guilds) {
try {
const guildConfig = getGuildConfig(guildId);
Expand Down
44 changes: 36 additions & 8 deletions slashCommands/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,27 @@ module.exports = {
return interaction.editReply({ embeds: [embed] });
} else {
// Update Steam64 ID
linkUserGlobally(targetUser.id, steam64Id, targetUser.username);
const result = await linkUserGlobally(targetUser.id, steam64Id, targetUser.username, interaction.client);

let description = `Updated your Steam64 ID from \`${existingSteam64}\` to \`${steam64Id}\``;
if (result.dmEnabled) {
description += '\n\n✅ DM notifications enabled! Check your DMs for confirmation.';
} else if (result.dmError) {
description += '\n\n⚠️ **Warning:** Couldn\'t send you a DM. Please enable "Allow direct messages from server members" in your privacy settings if you want DM notifications.';
}

const embed = new EmbedBuilder()
.setColor('#00ff00')
.setTitle('Link Updated')
.setDescription(`Updated your Steam64 ID from \`${existingSteam64}\` to \`${steam64Id}\``);
.setDescription(description);
return interaction.editReply({ embeds: [embed] });
}
}

// Link user globally
if (!linkUserGlobally(targetUser.id, steam64Id, targetUser.username)) {
// Link user globally with DM testing
const linkResult = await linkUserGlobally(targetUser.id, steam64Id, targetUser.username, interaction.client);

if (!linkResult.success) {
const embed = new EmbedBuilder()
.setColor('#ff0000')
.setTitle('Error')
Expand All @@ -109,6 +119,13 @@ module.exports = {
};
matchTracker.saveTrackerData();

let note = 'Automatic roasting only works in servers where the bot is configured.';
if (linkResult.dmEnabled) {
note += '\n✅ DM notifications enabled! Check your DMs for confirmation.';
} else if (linkResult.dmError) {
note += '\n⚠️ DM notifications couldn\'t be enabled. Please enable "Allow direct messages from server members" in your privacy settings.';
}

const successEmbed = new EmbedBuilder()
.setColor('#00ff00')
.setTitle('Global Link Successful')
Expand All @@ -117,7 +134,7 @@ module.exports = {
{ name: 'Player', value: playerName },
{ name: 'Matches Tracked', value: currentMatchCount.toString() },
{ name: 'Available Commands', value: '`/stats` - View your stats\n`/roast` - Get instant roasts\n`/tracker check` - Check for new matches' },
{ name: 'Note', value: 'Automatic roasting only works in servers where the bot is configured.' },
{ name: 'Note', value: note },
);
await interaction.editReply({ embeds: [successEmbed] });

Expand Down Expand Up @@ -177,8 +194,10 @@ module.exports = {
// Check if user is already linked in this guild
const alreadyLinked = isUserLinkedInGuild(targetUser.id, interaction.guild.id);

// Save the link (guild-specific)
if (!linkUserToGuild(targetUser.id, interaction.guild.id, steam64Id, targetUser.username, interaction.user.id)) {
// Save the link (guild-specific) with DM testing
const linkResult = await linkUserToGuild(targetUser.id, interaction.guild.id, steam64Id, targetUser.username, interaction.user.id, interaction.client);

if (!linkResult.success) {
const embed = new EmbedBuilder()
.setColor('#ff0000')
.setTitle('Error')
Expand Down Expand Up @@ -255,14 +274,23 @@ module.exports = {
}

// Update the reply with embed
let status = 'Initial roast sent! Automatic tracking is now active.';
if (linkResult.dmEnabled) {
status += '\n\n✅ DM notifications enabled! User will receive DMs when new matches are detected.';
} else if (linkResult.dmError && mentionedUser) {
status += '\n\n⚠️ DM notifications couldn\'t be enabled for this user (they may need to enable DMs or install the app).';
} else if (linkResult.dmError && !mentionedUser) {
status += '\n\n⚠️ DM notifications couldn\'t be enabled. Please enable "Allow direct messages from server members" in your privacy settings.';
}

const successEmbed = new EmbedBuilder()
.setColor('#00ff00')
.setTitle('Link Successful')
.setDescription(`Successfully linked ${targetUser} to Steam64 ID: \`${steam64Id}\``)
.addFields(
{ name: 'Player', value: playerName },
{ name: 'Matches Tracked', value: currentMatchCount.toString() },
{ name: 'Status', value: 'Initial roast sent! Automatic tracking is now active.' },
{ name: 'Status', value: status },
);
await interaction.editReply({ embeds: [successEmbed] });

Expand Down
68 changes: 68 additions & 0 deletions slashCommands/optout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js');
const { disableDMRoasts, isDMRoastsEnabled } = require('../utils/userPreferencesManager');

module.exports = {
data: new SlashCommandBuilder()
.setName('optout')
.setDescription('Opt out of DM roast notifications')
.addSubcommand(subcommand =>
subcommand
.setName('dm_roasts')
.setDescription('Stop receiving roasts in DMs')),

// Mark as user-installable (works in DMs and guilds)
userInstallable: true,
guildOnly: false,

async execute(interaction) {
const subcommand = interaction.options.getSubcommand();

if (subcommand === 'dm_roasts') {
return this.handleDMRoastsOptOut(interaction);
}
},

async handleDMRoastsOptOut(interaction) {
const userId = interaction.user.id;

// Check if already opted out
if (!isDMRoastsEnabled(userId)) {
const embed = new EmbedBuilder()
.setColor('#ff9900')
.setTitle('Already Opted Out')
.setDescription('You\'re not currently receiving roasts in DMs.')
.addFields({
name: 'Want to opt back in?',
value: 'Run `/link` again with your Steam64 ID to re-enable DM notifications.',
});
return interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
}

// Disable DM roasts
if (!disableDMRoasts(userId)) {
const embed = new EmbedBuilder()
.setColor('#ff0000')
.setTitle('Error')
.setDescription('Failed to opt out. Please try again.');
return interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
}

const embed = new EmbedBuilder()
.setColor('#00ff00')
.setTitle('Opted Out Successfully')
.setDescription('✅ You will no longer receive roasts in DMs.')
.addFields(
{
name: 'Server Roasts',
value: 'You\'ll still get roasted in servers where you\'re linked.',
},
{
name: 'Re-enable DM Notifications',
value: 'Run `/link` again with your Steam64 ID to opt back in.',
},
);

await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
console.log(`[OPTOUT] User ${interaction.user.username} (${userId}) opted out of DM roasts`);
},
};
Loading