-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add spigot account linking, add requests
- Loading branch information
Showing
10 changed files
with
352 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,4 +13,7 @@ config.js | |
/.yarn/ | ||
|
||
# output files | ||
/build/ | ||
/build/ | ||
|
||
# data | ||
/data/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,33 @@ | ||
import { SlashCommandBuilder } from '@discordjs/builders'; | ||
import { CommandInteraction, PermissionResolvable } from 'discord.js'; | ||
|
||
export type CommandExecutor = (interaction: CommandInteraction) => Promise<void>; | ||
|
||
export interface Command { | ||
data: any, | ||
executor: CommandExecutor; | ||
} | ||
|
||
export default class CommandBuilder extends SlashCommandBuilder { | ||
|
||
private executor: CommandExecutor; | ||
private permissions: PermissionResolvable[]; | ||
|
||
setExecutor(executor: CommandExecutor): CommandBuilder { | ||
this.executor = executor; | ||
return this; | ||
} | ||
|
||
build(): Command { | ||
return { | ||
data: this.toJSON(), | ||
executor: this.executor | ||
}; | ||
} | ||
|
||
import { SlashCommandBuilder } from '@discordjs/builders'; | ||
import { CommandInteraction, PermissionResolvable } from 'discord.js'; | ||
|
||
export type CommandExecutor = (interaction: CommandInteraction) => Promise<void>; | ||
|
||
export interface Command { | ||
data: any, | ||
executor: CommandExecutor; | ||
} | ||
|
||
export default class CommandBuilder extends SlashCommandBuilder { | ||
|
||
private executor: CommandExecutor; | ||
private permissions: PermissionResolvable[]; | ||
|
||
setExecutor(executor: CommandExecutor): CommandBuilder { | ||
this.executor = executor; | ||
return this; | ||
} | ||
|
||
let(func: (_this: CommandBuilder) => any): this { | ||
func(this); | ||
return this; | ||
} | ||
|
||
build(): Command { | ||
return { | ||
data: this.toJSON(), | ||
executor: this.executor | ||
}; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import config from '../../../config.js'; | ||
import CommandBuilder from './command.builder'; | ||
import { FileSingletonDatastore } from '../../data/FileSingletonDatastore'; | ||
import {submit} from "../../task/tasks"; | ||
|
||
const discordToSpigotDatastore = new FileSingletonDatastore<string>('.', 'data', 'discord_to_spigot.json'); | ||
|
||
export default new CommandBuilder() | ||
.setName('request') | ||
.setDescription('Make a request to the developers!') | ||
.let(b => b.addSubcommand(builder => builder | ||
.setName('productroles') | ||
.setDescription('Request a verification of your purchases to get your bought products\' roles.'))) | ||
.setExecutor(async interaction => { | ||
const subcommand = interaction.options.getSubcommand(true); | ||
switch (subcommand) { | ||
case 'productroles': { | ||
const spigotId = await discordToSpigotDatastore.get(interaction.user.id); | ||
if (spigotId === null) { | ||
throw { | ||
title: 'You don\'t have a SpigotMC Forums account linked!', | ||
description: 'To link your SpigotMC Forums account, use the `/spigot` command.', | ||
color: config.color, | ||
ephemeral: true | ||
}; | ||
} | ||
await submit(interaction.client, { | ||
description: 'Verify my products and give me my roles.', | ||
submittedBy: { | ||
id: interaction.user.id, | ||
name: interaction.user.username, | ||
spigotId: spigotId | ||
} | ||
}); | ||
throw { | ||
title: 'Request sent', | ||
description: 'Your request has been sent to the developers, please wait patiently for an action.', | ||
color: config.color, | ||
ephemeral: true | ||
}; | ||
} | ||
default: { | ||
throw { | ||
title: 'Unknown request type', | ||
description: 'You tried to submit an unknown request type, please re-check the available sub-commands.', | ||
color: config.color, | ||
ephemeral: true | ||
}; | ||
} | ||
} | ||
}) | ||
.build(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import CommandBuilder from './command.builder'; | ||
import { FileSingletonDatastore } from '../../data/FileSingletonDatastore'; | ||
|
||
const discordToSpigotDatastore = new FileSingletonDatastore<string>('.', 'data', 'discord_to_spigot.json'); | ||
const spigotToDiscordDatastore = new FileSingletonDatastore<string>('.', 'data', 'spigot_to_discord.json'); | ||
|
||
export default new CommandBuilder() | ||
.setName('spigot-unlink') | ||
.setDescription('Unlink your SpigotMC Forums account from this Discord account') | ||
.setExecutor(async interaction => { | ||
const { user } = interaction; | ||
|
||
const spigotId = await discordToSpigotDatastore.get(user.id); | ||
if (spigotId === null) { | ||
throw { | ||
title: 'You don\'t have a SpigotMC Forums account linked!', | ||
description: 'To link your SpigotMC Forums account, use the `/spigot` command.' | ||
}; | ||
} | ||
|
||
await discordToSpigotDatastore.remove(user.id); | ||
await spigotToDiscordDatastore.remove(spigotId); | ||
throw { | ||
title: 'Unlinked SpigotMC Forums account!', | ||
description: 'You have successfully unlinked your SpigotMC Forums account.' | ||
}; | ||
}) | ||
.build(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import CommandBuilder from './command.builder'; | ||
import { findAuthor } from '../../util/spigot'; | ||
import { FileSingletonDatastore } from '../../data/FileSingletonDatastore'; | ||
|
||
const discordToSpigotDatastore = new FileSingletonDatastore<string>('.', 'data', 'discord_to_spigot.json'); | ||
const spigotToDiscordDatastore = new FileSingletonDatastore<string>('.', 'data', 'spigot_to_discord.json'); | ||
|
||
export default new CommandBuilder() | ||
.setName('spigot') | ||
.setDescription('Verify your SpigotMC Forums account') | ||
.addStringOption( | ||
option => option | ||
.setName('username') | ||
.setDescription('Your exact SpigotMC Forums username') | ||
.setRequired(true) | ||
) | ||
.setExecutor(async interaction => { | ||
const { user } = interaction; | ||
|
||
{ | ||
// check if this Discord user has a SpigotMC Forums account linked already | ||
const spigotId = await discordToSpigotDatastore.get(user.id); | ||
if (spigotId !== null) { | ||
throw { | ||
title: 'You already have a SpigotMC Forums account linked!', | ||
description: 'If you want to link a different account, unlink your current one first using the `/spigot-unlink` command.', | ||
ephemeral: true | ||
}; | ||
} | ||
} | ||
|
||
const username = interaction.options.getString('username'); | ||
const author = await findAuthor(username); | ||
if ((author as any).code === 404) { | ||
throw { title: 'Invalid SpigotMC Forums username', description: 'Can\'t find a SpigotMC author with that username.', ephemeral: true }; | ||
} | ||
|
||
{ | ||
// check if this SpigotMC Forums account is linked to a Discord user already | ||
const discordId = await spigotToDiscordDatastore.get(author.id); | ||
if (discordId !== null) { | ||
throw { | ||
title: 'This SpigotMC Forums account is already linked!', | ||
description: 'This SpigotMC Forums account is already verified for another Discord user.', | ||
ephemeral: true | ||
}; | ||
} | ||
} | ||
|
||
const senderUsername = interaction.user.username; | ||
const expectedUsername = author?.identities?.discord; | ||
|
||
if (expectedUsername === undefined) { | ||
throw { | ||
title: 'Can\'t verify SpigotMC Forums account: No Discord Identity', | ||
description: 'You have not linked your Discord account to your SpigotMC account. (from SpigotMC Forums).\n\n' | ||
+ 'Go to [your Contact Details page](https://www.spigotmc.org/account/contact-details) and set your ' | ||
+ 'Discord identity to `' + senderUsername + '`.', | ||
footer: { | ||
text: 'Please note that it can take some minutes until it fully updates.' | ||
}, | ||
ephemeral: true | ||
}; | ||
} | ||
|
||
if (senderUsername === expectedUsername) { | ||
await discordToSpigotDatastore.save(interaction.user.id, author.id); | ||
await spigotToDiscordDatastore.save(author.id, interaction.user.id); | ||
throw { | ||
title: `Verified SpigotMC Forums account: ${author.username}`, | ||
description: 'You have successfully verified your SpigotMC Forums account. If you' + | ||
' have bought a premium resource, you can now use the `/request` command to request your products\' roles.', | ||
author: { | ||
name: author?.username, | ||
iconURL: author?.avatar | ||
}, | ||
ephemeral: true | ||
}; | ||
} else { | ||
throw { | ||
title: 'Can\'t verify SpigotMC Forums account: Different Discord Identity', | ||
description: `The Discord identity set on SpigotMC Forums for ${author.username} is \`${expectedUsername}\`, but you are \`${senderUsername}\`.`, | ||
footer: { | ||
text: 'Please note that updates can take some minutes until they can be recognized by the bot.' | ||
}, | ||
ephemeral: true | ||
}; | ||
} | ||
}) | ||
.build(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* Represents a datastore, which is a repository for a specific | ||
* type of data. This can be a database, a file, or an in-memory | ||
* cache. | ||
* | ||
* @template T The type of the data | ||
*/ | ||
interface Datastore<T> { | ||
/** | ||
* Gets the value for the given key. | ||
* | ||
* @param key The key to get the value for | ||
* @returns A promise that resolves to the value for the given key | ||
*/ | ||
get(key: string): Promise<T | null>; | ||
|
||
/** | ||
* Removes the value for the given key. | ||
* | ||
* @param key The key to remove the value for | ||
*/ | ||
remove(key: string): Promise<void>; | ||
|
||
/** | ||
* Saves the given value for the given key. | ||
* | ||
* @param key The key to save the value for | ||
* @param value The value to save | ||
*/ | ||
save(key: string, value: T): Promise<void>; | ||
} | ||
|
||
export default Datastore; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import Datastore from './Datastore'; | ||
|
||
export class FileSingletonDatastore<T> implements Datastore<T> { | ||
private readonly file: string; | ||
|
||
constructor(...pathArgs: string[]) { | ||
this.file = path.join(...pathArgs); | ||
} | ||
|
||
async get(key: string): Promise<T | null> { | ||
if (!fs.existsSync(this.file)) { | ||
return null; | ||
} | ||
const content = fs.readFileSync(this.file, { encoding: 'utf-8' }); | ||
const json = JSON.parse(content); | ||
if (typeof json === 'object') { | ||
return json[key] || null; | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
async save(key: string, value: T): Promise<void> { | ||
let json; | ||
if (!fs.existsSync(this.file)) { | ||
json = {}; | ||
} else { | ||
const content = fs.readFileSync(this.file, { encoding: 'utf-8' }); | ||
json = JSON.parse(content); | ||
if (typeof json !== 'object') { | ||
json = {}; | ||
} | ||
} | ||
json[key] = value; | ||
|
||
// create directory if not exists | ||
const dir = path.dirname(this.file); | ||
if (!fs.existsSync(dir)) { | ||
fs.mkdirSync(dir, { recursive: true }); | ||
} | ||
|
||
fs.writeFileSync(this.file, JSON.stringify(json)); | ||
} | ||
|
||
async remove(key: string): Promise<void> { | ||
if (!fs.existsSync(this.file)) { | ||
return; | ||
} | ||
const content = fs.readFileSync(this.file, { encoding: 'utf-8' }); | ||
const json = JSON.parse(content); | ||
if (typeof json === 'object') { | ||
delete json[key]; | ||
fs.writeFileSync(this.file, JSON.stringify(json)); | ||
} | ||
} | ||
} |
Oops, something went wrong.