Skip to content

Commit

Permalink
Add patreon status claiming (#226)
Browse files Browse the repository at this point in the history
* Discount unranked metrics in equality checks

* Add "claim patreon benefits" modal
  • Loading branch information
psikoi authored Nov 27, 2023
1 parent cc93a3c commit 89114f8
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"canvas": "^2.6.1",
"cors": "^2.8.5",
"discord-api-types": "^0.27.2",
"discord.js": "^13.6.0",
"discord.js": "^13.7.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"lodash": "^4.17.19",
Expand Down
25 changes: 23 additions & 2 deletions src/bot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Client, Guild, Intents, MessageEmbed, TextChannel } from 'discord.js';
import { Client, Guild, Intents, Interaction, MessageEmbed, TextChannel } from 'discord.js';
import config from './config';
import * as router from './commands/router';
import {
PATREON_MODAL_ID,
PATREON_TRIGGER_ID,
handlePatreonModalSubmit,
handlePatreonTrigger,
setupPatreonTrigger
} from './patreon-trigger';

class Bot {
client: Client;
Expand All @@ -26,13 +33,27 @@ class Bot {
this.client.user?.setActivity('bot.wiseoldman.net');

// Send received interaction to the command router
this.client.on('interactionCreate', router.onInteractionReceived);
this.client.on('interactionCreate', (interaction: Interaction) => {
if (interaction.isButton() && interaction.customId === PATREON_TRIGGER_ID) {
handlePatreonTrigger(interaction);
return;
}

if (interaction.isModalSubmit() && interaction.customId === PATREON_MODAL_ID) {
handlePatreonModalSubmit(interaction);
return;
}

router.onInteractionReceived(interaction);
});

this.client.on('guildCreate', guild => {
const openChannel = findOpenChannel(guild);
if (openChannel) openChannel.send({ embeds: [buildJoinMessage()] });
});

setupPatreonTrigger(this.client);

console.log('Bot is running.');
});

Expand Down
5 changes: 4 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ export default {
roles: {
moderator: '705821689526747136',
groupLeader: '705826389474934845',
apiConsumer: '713452544164233296'
apiConsumer: '713452544164233296',
patreonSupporter: '1169327347032412300',
patreonSupporterT2: '1178310263154417735'
},
channels: {
flags: '802680940835897384',
modLogs: '830199626630955039',
patreonInfo: '1173680059526152272',
flaggedPlayerReviews: '1086637095415722169'
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/content/patreon-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
To keep Wise Old Man free without any ads, we depend on generous support from our patrons to cover our servers costs. Please consider supporting us.

<https://wiseoldman.net/patreon>

# Benefits

### Tier 1 ($3)

```
Support the project, flex your status on the website/Discord AND auto-update your profile everyday? Sounds like a deal to me!
- Daily auto-update for your player profile
- "Patreon Supporter" Badge on your player profile
- "Patreon Supporter" Discord Role
```

### Tier 2 ($5.99)

```
With this generous pledge, you'll be literally keeping the lights on. As a reward, your group stays up to date and stands out from the others!
- Every Tier 1 benefit
- Daily auto-update for all your group members
- Custom Avatar and Banner on your group page
- Social links on your group page
- "Patreon Supporter" Badge on your group page
- Higher ranking and visibility on group page searches
```

# How to claim your benefits

After subscribing to our [Patreon](https://wiseoldman.net/patreon), you should connect your Discord account on Patreon (
see a guide [here](https://support.patreon.com/hc/en-us/articles/212052266-Getting-Discord-access>)), and then you can click the button below to claim your benefits.

** **
10 changes: 5 additions & 5 deletions src/events/instances/PlayerFlaggedReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,16 @@ class PlayerFlaggedReview implements Event {
return null;
});

const equalityCount = sameMetrics.filter(v => v !== null).length;
const unrankedCount = sameMetrics.filter(v => v === -1).length;

const equalityPercent = Math.round((equalityCount / realMetrics.length) * 100);
const rankedCount = realMetrics.length - unrankedCount;
const equalityCount = sameMetrics.filter(v => v !== null).length - unrankedCount;

const equalityPercent = Math.round((equalityCount / rankedCount) * 100);

lines.push(`\n`);
lines.push(`**Equality:**`);
lines.push(
`${equalityCount}/${realMetrics.length} **(${equalityPercent}%)** (${unrankedCount} unranked)`
);
lines.push(`${equalityCount}/${rankedCount} **(${equalityPercent}%)** (${unrankedCount} unranked)`);

lines.push(...getLargestSkillChanges(previous, rejected));
lines.push(...getLargestBossChanges(previous, rejected));
Expand Down
124 changes: 124 additions & 0 deletions src/patreon-trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import fs from 'fs';
import path from 'path';
import {
ButtonInteraction,
Client,
GuildMember,
MessageActionRow,
MessageButton,
Modal,
ModalSubmitInteraction,
TextInputComponent
} from 'discord.js';
import config from './config';
import { hasRole } from './utils';
import { claimBenefits } from './services/wiseoldman';

export const PATREON_MODAL_ID = 'patreon-benefits-modal';
export const PATREON_TRIGGER_ID = 'patreon-benefits-trigger';

const NOT_A_PATRON_ERROR_MESSAGE = `Only Patreon supporters can claim benefits, please consider helping fund the project at https://wiseoldman.net/patreon.\n\nIf you already are a Patreon supporter, make sure to connect your Discord account to your Patreon account.`;

export async function setupPatreonTrigger(client: Client) {
const patreonInfoChannel = client.channels.cache.get(config.discord.channels.patreonInfo);
if (!patreonInfoChannel?.isText()) return;

const messages = await patreonInfoChannel.messages.fetch({ limit: 100 });
const botMessages = messages.filter(msg => msg.author.id === client.user?.id);

if (botMessages.size !== 0) {
// Already posted the Patreon Info message.
return;
}

const content = fs.readFileSync(path.join('src', 'content', 'patreon-info.md'), 'utf8');

const actions = new MessageActionRow().addComponents(
new MessageButton()
.setCustomId(PATREON_TRIGGER_ID)
.setLabel('Claim Patreon Benefits')
.setStyle('SUCCESS')
);

const message = await patreonInfoChannel.send({ content, components: [actions] });
await message.suppressEmbeds(true);
}

export async function handlePatreonTrigger(interaction: ButtonInteraction) {
if (!interaction.member) return;

const member = interaction.member as GuildMember;

if (!hasRole(member, config.discord.roles.patreonSupporter)) {
await interaction.reply({ content: NOT_A_PATRON_ERROR_MESSAGE, ephemeral: true });
return;
}

const isTier2Supporter = hasRole(member, config.discord.roles.patreonSupporterT2);

const modal = new Modal()
.setCustomId(PATREON_MODAL_ID)
.setTitle(`Claim Patreon Benefits (Tier ${isTier2Supporter ? 2 : 1})`);

const usernameInput = new TextInputComponent()
.setCustomId('username')
.setLabel('Your in-game username')
.setPlaceholder('Ex: Zezima')
.setMaxLength(12)
.setStyle(1)
.setRequired(true);

const groupIdInput = new TextInputComponent()
.setCustomId('groupId')
.setLabel("Your group's ID")
.setPlaceholder("Ex: 139 (Can be found on your group's page URL.)")
.setStyle(1);

// @ts-expect-error -- Typings are wrong on discord.js v13.7.0 (can be deleted on a v14 upgrade)
modal.addComponents(new MessageActionRow().addComponents(usernameInput));

if (isTier2Supporter) {
// @ts-expect-error -- Typings are wrong on discord.js v13.7.0 (can be deleted on a v14 upgrade)
modal.addComponents(new MessageActionRow().addComponents(groupIdInput));
}

interaction.showModal(modal);
}

export async function handlePatreonModalSubmit(interaction: ModalSubmitInteraction) {
const username = interaction.fields.getTextInputValue('username');
const groupIdValue = interaction.fields.getTextInputValue('groupId');

let groupId: number | undefined;

if (!username) {
interaction.reply({ content: '❌ Please provide your in-game username.', ephemeral: true });
return;
}

if (groupIdValue) {
const isInteger = typeof groupIdValue === 'string' && Number.isInteger(parseInt(groupIdValue));

if (!isInteger) {
interaction.reply({ content: '❌ Please provide a valid group ID.', ephemeral: true });
return;
}

groupId = parseInt(groupIdValue);
}

try {
await claimBenefits(interaction.user.id, username, groupId);

let successMessage = '✅ Your benefits have been claimed!';

if (groupId) {
successMessage += ` You can edit your group's images and social links on your group's edit page on the website.`;
}

interaction.reply({ content: successMessage, ephemeral: true });
} catch (error) {
console.log(error);
interaction.reply({ content: error.message, ephemeral: true });
}
}
12 changes: 12 additions & 0 deletions src/services/wiseoldman.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ export async function createAPIKey(application: string, developer: string): Prom
});
}

export async function claimBenefits(
discordId: string,
username: string,
groupId?: number
): Promise<void> {
return womClient.putRequest(`/patrons/claim/${discordId}`, {
username,
groupId,
adminPassword: env.ADMIN_PASSWORD
});
}

/**
* Send an API request attempting to update a player's country
*/
Expand Down
8 changes: 6 additions & 2 deletions src/utils/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,11 +391,15 @@ export function isAdmin(member: GuildMember | null): boolean {
return member ? member?.permissions.has('ADMINISTRATOR') : false;
}

export function hasModeratorRole(member: GuildMember | null): boolean {
export function hasRole(member: GuildMember | null, roleId: string): boolean {
if (!member) return false;
if (!member.roles || !member.roles.cache) return false;

return member.roles.cache.some(r => r.id === config.discord.roles.moderator);
return member.roles.cache.some(r => r.id === roleId);
}

export function hasModeratorRole(member: GuildMember | null): boolean {
return hasRole(member, config.discord.roles.moderator);
}

export function getMissingPermissions(channel: TextChannel) {
Expand Down

0 comments on commit 89114f8

Please sign in to comment.