Add things
This commit is contained in:
commit
9d9f0e2e91
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
productFiles/*
|
109
commands.js
Normal file
109
commands.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Command definitions
|
||||||
|
const Discord = require("discord.js")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
global: [
|
||||||
|
// Use SlashCommandBuilder for command creation
|
||||||
|
{
|
||||||
|
name: "give",
|
||||||
|
description: "Give a product to a user",
|
||||||
|
default_member_permissions: 0,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "product-name",
|
||||||
|
description: "The name of the product",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "roblox-id",
|
||||||
|
description: "The Roblox ID of the user",
|
||||||
|
type: Discord.ApplicationCommandOptionType.Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discord-id",
|
||||||
|
description: "The Discord ID of the user",
|
||||||
|
type: Discord.ApplicationCommandOptionType.User,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "revoke",
|
||||||
|
description: "Revoke a product from a user",
|
||||||
|
default_member_permissions: 0,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "product-name",
|
||||||
|
description: "The name of the product",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "roblox-id",
|
||||||
|
description: "The Roblox ID of the user",
|
||||||
|
type: Discord.ApplicationCommandOptionType.Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discord-id",
|
||||||
|
description: "The Discord ID of the user",
|
||||||
|
type: Discord.ApplicationCommandOptionType.User,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "link",
|
||||||
|
description: "Link your Roblox account",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "pairing-code",
|
||||||
|
description: "The pairing code given to you from the Roblox game",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unlink",
|
||||||
|
description: "Unlink your Roblox account",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "products",
|
||||||
|
description: "List all products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "profile",
|
||||||
|
description: "View a user's profile",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "roblox-id",
|
||||||
|
description: "The Roblox ID of the user",
|
||||||
|
type: Discord.ApplicationCommandOptionType.Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discord-id",
|
||||||
|
description: "The Discord ID of the user",
|
||||||
|
type: Discord.ApplicationCommandOptionType.User,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "retrieve",
|
||||||
|
description: "Retrieve an owned product",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "product-name",
|
||||||
|
description: "The name of the product",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
admin: []
|
||||||
|
}
|
36
commands/give.js
Normal file
36
commands/give.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
var robloxID = interaction.options.getNumber("roblox-id") || null;
|
||||||
|
const discordID = interaction.options.getUser("discord-id")?.id || null;
|
||||||
|
if (!robloxID && !discordID) return interaction.reply({ content: "You must provide a Roblox ID or Discord ID", ephemeral: true });
|
||||||
|
|
||||||
|
// If both robloxID and discordID are provided, we can't continue
|
||||||
|
if (robloxID && discordID) return interaction.reply({ content: "You can't provide both a Roblox ID and Discord ID", ephemeral: true });
|
||||||
|
|
||||||
|
// If discordID is provided, we need to find the robloxID
|
||||||
|
if (discordID) {
|
||||||
|
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
|
||||||
|
if (!row) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
robloxID = row.robloxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user exists
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
|
||||||
|
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
|
||||||
|
const productName = interaction.options.getString("product-name");
|
||||||
|
// try catch try and find the product based on partial product name, parse everything in uppercase to make things easier
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]);
|
||||||
|
if (!product) return interaction.reply({ content: "Product not found", ephemeral: true });
|
||||||
|
// Check if the user already owns the product
|
||||||
|
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
|
||||||
|
if (purchase) return interaction.reply({ content: "User already owns this product", ephemeral: true });
|
||||||
|
|
||||||
|
// Insert purchase into database
|
||||||
|
await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [robloxID, product.id, product.hubId]);
|
||||||
|
return interaction.reply({ content: `Gave \`${product.name}\` to ${robloxID}`, ephemeral: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { execute }
|
15
commands/link.js
Normal file
15
commands/link.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
const pairingCode = interaction.options.getString("pairing-code");
|
||||||
|
const discordID = interaction.user.id;
|
||||||
|
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
|
||||||
|
if (row) return interaction.reply({ content: "You have already linked your account", ephemeral: true });
|
||||||
|
const [pairing] = await pool.query('SELECT * FROM users WHERE pairingCode = ?', [pairingCode]);
|
||||||
|
if (!pairing) return interaction.reply({ content: "Invalid pairing code", ephemeral: true });
|
||||||
|
await pool.query('UPDATE users SET discordId = ?, discordDisplayName = ?, pairingCode = NULL WHERE pairingCode = ?', [discordID, interaction.user.displayName, pairingCode]);
|
||||||
|
return interaction.reply({ content: "Successfully linked your account", ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { execute }
|
24
commands/products.js
Normal file
24
commands/products.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
try {
|
||||||
|
const [hub] = await pool.query(`SELECT * FROM hubs WHERE discordGuild = ?`, [interaction.guildId])
|
||||||
|
if (!hub) return interaction.reply({ content: "This server doesn't have a hub", ephemeral: true });
|
||||||
|
const products = await pool.query(`SELECT * FROM products WHERE hubId = ?`, [hub.id]);
|
||||||
|
const embed = {
|
||||||
|
title: hub.name,
|
||||||
|
fields: products.map(product => ({
|
||||||
|
name: product.name,
|
||||||
|
value: product.description
|
||||||
|
})),
|
||||||
|
color: 0x2F3136
|
||||||
|
}
|
||||||
|
return interaction.reply({ embeds: [embed] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return interaction.reply({ content: "An error occurred while fetching products", ephemeral: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { execute }
|
44
commands/profile.js
Normal file
44
commands/profile.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
var robloxID = interaction.options.getNumber("roblox-id") || null;
|
||||||
|
const discordID = interaction.options.getUser("discord-id")?.id || null;
|
||||||
|
if (!robloxID && !discordID) return interaction.reply({ content: "You must provide a Roblox ID or Discord ID", ephemeral: true });
|
||||||
|
|
||||||
|
// If both robloxID and discordID are provided, we can't continue
|
||||||
|
if (robloxID && discordID) return interaction.reply({ content: "You can't provide both a Roblox ID and Discord ID", ephemeral: true });
|
||||||
|
|
||||||
|
// If discordID is provided, we need to find the robloxID
|
||||||
|
if (discordID) {
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
|
||||||
|
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
robloxID = user.robloxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user exists
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
|
||||||
|
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
|
||||||
|
// Check if the user already owns the product
|
||||||
|
const purchases = await pool.query('SELECT * FROM purchases WHERE robloxId = ?', [robloxID]);
|
||||||
|
const products = await pool.query('SELECT * FROM products');
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
title: `Profile for ${user.discordDisplayName}`,
|
||||||
|
description: `Roblox ID: ${robloxID}`,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: `Owned Products (${purchases.length})`,
|
||||||
|
value: purchases.map(purchase => {
|
||||||
|
const product = products.find(product => product.id === purchase.productId);
|
||||||
|
return product.name;
|
||||||
|
}).join("\n")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return interaction.reply({ embeds: [embed] });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { execute }
|
57
commands/retrieve.js
Normal file
57
commands/retrieve.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
// If discordID is provided, we need to find the robloxID
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE discordId = ?', [interaction.user.id]);
|
||||||
|
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
robloxID = user.robloxId;
|
||||||
|
|
||||||
|
// Check if the user exists
|
||||||
|
if (!robloxID) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
|
||||||
|
const productName = interaction.options.getString("product-name");
|
||||||
|
// try catch try and find the product based on partial product name, parse everything in uppercase to make things easier
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]);
|
||||||
|
if (!product) return interaction.reply({ content: "Product not found", ephemeral: true });
|
||||||
|
// Check if the user already owns the product
|
||||||
|
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
|
||||||
|
if (!purchase) return interaction.reply({ content: `You don't own \`${product.name}\``, ephemeral: true });
|
||||||
|
|
||||||
|
const [auth] = await pool.query('SELECT * FROM fileAuth WHERE owner = ? and product = ?', [robloxID, product.id]);
|
||||||
|
if (auth && auth.expires > Date.now()) return interaction.reply({ content: `Download \`${product.name}\``, ephemeral: true, components: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
label: 'Download',
|
||||||
|
style: 5,
|
||||||
|
url: `https://cdn.example.com/${product.file}/${authToken}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
] });
|
||||||
|
// Use crypto to generate 32 character auth token
|
||||||
|
const authToken = crypto.randomBytes(16).toString('hex');
|
||||||
|
// Insert the new auth token into the fileAuth table with an expiration date of 24 hours from now
|
||||||
|
await pool.query('INSERT INTO fileAuth (owner, product, token, expires) VALUES (?, ?, ?, ?)', [robloxID, product.id, authToken, Date.now() + 24 * 60 * 60 * 1000]);
|
||||||
|
|
||||||
|
// Reply with the download link
|
||||||
|
return interaction.reply({ content: `Download \`${product.name}\``, ephemeral: true, components: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
label: 'Download',
|
||||||
|
style: 5,
|
||||||
|
url: `https://cdn.example.com/${product.file}/${authToken}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
] });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { execute }
|
36
commands/revoke.js
Normal file
36
commands/revoke.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
var robloxID = interaction.options.getNumber("roblox-id") || null;
|
||||||
|
const discordID = interaction.options.getUser("discord-id")?.id || null;
|
||||||
|
if (!robloxID && !discordID) return interaction.reply({ content: "You must provide a Roblox ID or Discord ID", ephemeral: true });
|
||||||
|
|
||||||
|
// If both robloxID and discordID are provided, we can't continue
|
||||||
|
if (robloxID && discordID) return interaction.reply({ content: "You can't provide both a Roblox ID and Discord ID", ephemeral: true });
|
||||||
|
|
||||||
|
// If discordID is provided, we need to find the robloxID
|
||||||
|
if (discordID) {
|
||||||
|
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
|
||||||
|
if (!row) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
robloxID = row.robloxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user exists
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
|
||||||
|
if (!user) return interaction.reply({ content: "User not found", ephemeral: true });
|
||||||
|
|
||||||
|
const productName = interaction.options.getString("product-name");
|
||||||
|
// try catch try and find the product based on partial product name, parse everything in uppercase to make things easier
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE UPPER(name) LIKE ?', [`%${productName.toUpperCase()}%`]);
|
||||||
|
if (!product) return interaction.reply({ content: "Product not found", ephemeral: true });
|
||||||
|
// Check if the user already owns the product
|
||||||
|
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
|
||||||
|
if (!purchase) return interaction.reply({ content: "User doesn't own product", ephemeral: true });
|
||||||
|
|
||||||
|
// Remove purchase from database
|
||||||
|
await pool.query('DELETE FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
|
||||||
|
return interaction.reply({ content: `Removed \`${product.name}\` from ${robloxID}`, ephemeral: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { execute }
|
12
commands/unlink.js
Normal file
12
commands/unlink.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const client = global.discord_client
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
const execute = async (interaction) => {
|
||||||
|
const discordID = interaction.user.id;
|
||||||
|
const [row] = await pool.query('SELECT * FROM users WHERE discordId = ?', [discordID]);
|
||||||
|
if (!row) return interaction.reply({ content: "Your account isn't linked!", ephemeral: true });
|
||||||
|
await pool.query('UPDATE users SET discordId = NULL, pairingCode = NULL WHERE discordId = ?', [discordID]);
|
||||||
|
return interaction.reply({ content: "Successfully unlinked your account. Please use the hub game to get a new pairing code!", ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { execute }
|
105
index.js
Normal file
105
index.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
require("dotenv").config();
|
||||||
|
const MariaDB = require('mariadb');
|
||||||
|
const fs = require('fs');
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const log = require("./log")
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
global.log = log;
|
||||||
|
|
||||||
|
const Discord = require("discord.js")
|
||||||
|
const client = new Discord.Client({intents: ["Guilds", "DirectMessages"]})
|
||||||
|
|
||||||
|
const pool = MariaDB.createPool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
connectionLimit: 50 // Overkill but why not lol
|
||||||
|
});
|
||||||
|
global.db_pool = pool; // For access in other files
|
||||||
|
|
||||||
|
// Middleware to parse incoming JSON requests
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Middleware to log requests
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (process.env.NODE_ENV === 'production') return next();
|
||||||
|
log.info('--- Incoming Request ---');
|
||||||
|
log.info(`Method: ${req.method}`);
|
||||||
|
log.info(`URL: ${req.url}`);
|
||||||
|
log.info('Headers:', req.headers);
|
||||||
|
if (Object.keys(req.body).length > 0) {
|
||||||
|
log.info('Body:', req.body);
|
||||||
|
}
|
||||||
|
log.info('------------------------');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
global.discord_client = client
|
||||||
|
client.on("ready", async () => {
|
||||||
|
log.info(`Logged into Discord as ${client.user.displayName}`);
|
||||||
|
const commands = require("./commands")
|
||||||
|
// Command registration
|
||||||
|
log.info("Registering commands...")
|
||||||
|
await (async () => {
|
||||||
|
try {
|
||||||
|
const rest = new Discord.REST().setToken(client.token);
|
||||||
|
//Global
|
||||||
|
//await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: [] })
|
||||||
|
log.info(`Registering global commands`);
|
||||||
|
rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands.global }).then(() => {
|
||||||
|
log.info("Global commands registered")
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error(error)
|
||||||
|
});
|
||||||
|
|
||||||
|
//Admin
|
||||||
|
// rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: commands.admin }).then(() => {
|
||||||
|
// log.info("Admin commands registered")
|
||||||
|
// }).catch((error) => {
|
||||||
|
// log.error(error)
|
||||||
|
// });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error)
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
log.info(`Listening on ${port}`)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("interactionCreate", async (interaction) => {
|
||||||
|
if (!interaction.isCommand()) return;
|
||||||
|
const command = require(`./commands/${interaction.commandName}`);
|
||||||
|
|
||||||
|
if (!command) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await command.execute(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error.stack);
|
||||||
|
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const port = process.env.SERVER_PORT || 3000;
|
||||||
|
|
||||||
|
require("./migrations")(pool)
|
||||||
|
.finally(() => {
|
||||||
|
// Load all route modules from the 'routes' folder
|
||||||
|
const routesPath = path.join(__dirname, 'routes');
|
||||||
|
fs.readdirSync(routesPath).forEach((file) => {
|
||||||
|
const route = require(path.join(routesPath, file));
|
||||||
|
const routeName = `/${file.replace('.js', '')}`; // Use filename as route base
|
||||||
|
app.use(routeName, route);
|
||||||
|
log.info(`Using ${routeName}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(process.env.DISCORD_TOKEN);
|
||||||
|
});
|
9
log.js
Normal file
9
log.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const colors = require("colors");
|
||||||
|
module.exports = {
|
||||||
|
info(msg) {
|
||||||
|
console.log(`${colors.cyan.bold("[INFO]")} ${msg}`);
|
||||||
|
},
|
||||||
|
error(msg) {
|
||||||
|
console.log(`${colors.red.bold("[ERROR]")} ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
72
migrations.js
Normal file
72
migrations.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
const mariadb = require('mariadb');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const util = require("util")
|
||||||
|
const log = require("./log")
|
||||||
|
|
||||||
|
function runMigrations(pool) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
pool.getConnection()
|
||||||
|
.then(conn => {
|
||||||
|
connection = conn;
|
||||||
|
|
||||||
|
// Ensure a migrations table exists to track applied migrations
|
||||||
|
return connection.query(`CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);`);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Read all migration files
|
||||||
|
const migrationDir = path.join(__dirname, 'migrations');
|
||||||
|
const files = fs.readdirSync(migrationDir).sort(); // Sort to apply in order
|
||||||
|
|
||||||
|
return files.reduce((promise, file) => {
|
||||||
|
return promise.then(() => {
|
||||||
|
const migrationName = path.basename(file);
|
||||||
|
|
||||||
|
// Check if the migration has already been applied
|
||||||
|
return connection.query(
|
||||||
|
'SELECT 1 FROM migrations WHERE name = ? LIMIT 1',
|
||||||
|
[migrationName]
|
||||||
|
).then(([rows]) => {
|
||||||
|
if (Object.keys(rows || {}).length > 0) {
|
||||||
|
//console.log(`Skipping already applied migration: ${migrationName}`);
|
||||||
|
return; // Skip this migration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and execute the migration SQL
|
||||||
|
const migrationPath = path.join(migrationDir, file);
|
||||||
|
const sql = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
return connection.query(sql).then(() => {
|
||||||
|
// Record the applied migration
|
||||||
|
return connection.query(
|
||||||
|
'INSERT INTO migrations (name) VALUES (?)',
|
||||||
|
[migrationName]
|
||||||
|
).then(() => {
|
||||||
|
log.info(`Applied migration: ${migrationName}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
log.info('All migrations applied successfully!');
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
log.error('Error running migrations:', err);
|
||||||
|
reject(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (connection) connection.release();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = runMigrations
|
7
migrations/001_generate_user_table.sql
Normal file
7
migrations/001_generate_user_table.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
robloxId BIGINT PRIMARY KEY,
|
||||||
|
discordId BIGINT DEFAULT NULL,
|
||||||
|
pairingCode VARCHAR(6) DEFAULT NULL,
|
||||||
|
discordDisplayName VARCHAR(48) DEFAULT 'Unknown',
|
||||||
|
UNIQUE (robloxId, discordId)
|
||||||
|
)
|
12
migrations/002_generate_hubs_table.sql
Normal file
12
migrations/002_generate_hubs_table.sql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS hubs (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT UUID(),
|
||||||
|
ownerId BIGINT, -- Owner of the hub (Discord ID)
|
||||||
|
discordGuild BIGINT, -- Discord Guild ID
|
||||||
|
name VARCHAR(128), -- Name of the hub
|
||||||
|
shortDescription VARCHAR(256), -- Short description of the hub
|
||||||
|
longDescription TEXT, -- Long description of the hub
|
||||||
|
allowGiftPurchase BOOLEAN DEFAULT TRUE, -- Allow users to buy gifts for others on this hub.
|
||||||
|
tos TEXT DEFAULT 'This Hub does not have any Terms of Service yet. If you are the Hub owner, you can update this under settings.', -- Terms of Service
|
||||||
|
bgmId BIGINT DEFAULT NULL, -- Background Music ID
|
||||||
|
secretKey VARCHAR(128) UNIQUE NOT NULL -- Secret key for the hub to authenticate
|
||||||
|
)
|
13
migrations/003_generate_products_table.sql
Normal file
13
migrations/003_generate_products_table.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT UUID(),
|
||||||
|
hubId CHAR(36), -- Hub that the product belongs to
|
||||||
|
name VARCHAR(128), -- Name of the product
|
||||||
|
description TEXT, -- Description of the product
|
||||||
|
devProductID BIGINT NOT NULL, -- Dev Product ID
|
||||||
|
decalId BIGINT DEFAULT NULL, -- Decal ID
|
||||||
|
stock BIGINT DEFAULT -1, -- Stock of the product (If enabled)
|
||||||
|
category VARCHAR(64) DEFAULT NULL, -- Category of the product
|
||||||
|
file VARCHAR(36) DEFAULT NULL, -- File ID for the product
|
||||||
|
fileType VARCHAR(16) DEFAULT NULL, -- File type of the product
|
||||||
|
FOREIGN KEY (hubId) REFERENCES hubs(id)
|
||||||
|
)
|
10
migrations/004_generate_purchases_table.sql
Normal file
10
migrations/004_generate_purchases_table.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS purchases (
|
||||||
|
id CHAR(36) PRIMARY KEY DEFAULT UUID(),
|
||||||
|
robloxId BIGINT, -- User that made the purchase
|
||||||
|
productId CHAR(36), -- Product that was purchased
|
||||||
|
hubId CHAR(36), -- Hub that the product belongs to
|
||||||
|
FOREIGN KEY (robloxId) REFERENCES users(robloxId),
|
||||||
|
FOREIGN KEY (productId) REFERENCES products(id),
|
||||||
|
FOREIGN KEY (hubId) REFERENCES hubs(id),
|
||||||
|
UNIQUE (productId, robloxId)
|
||||||
|
)
|
8
migrations/005_generate_fileAuth_table.sql
Normal file
8
migrations/005_generate_fileAuth_table.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS fileAuth (
|
||||||
|
token CHAR(64) PRIMARY KEY NOT NULL, -- Single use token to download the file
|
||||||
|
product VARCHAR(36) NOT NULL, -- Product ID (Reference to product in products table)
|
||||||
|
owner BIGINT NOT NULL, -- Owner of the token
|
||||||
|
expires BIGINT NOT NULL, -- Expiration for this token
|
||||||
|
FOREIGN KEY (owner) REFERENCES users(robloxId),
|
||||||
|
FOREIGN KEY (product) REFERENCES products(id)
|
||||||
|
)
|
4
migrations/990_test_user.sql
Normal file
4
migrations/990_test_user.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-- Insert a user
|
||||||
|
INSERT INTO users (robloxId, discordId, pairingCode, discordDisplayName)
|
||||||
|
VALUES (25226480, 289884287765839882, NULL, 'Chris C');
|
||||||
|
|
3
migrations/991_test_hub.sql
Normal file
3
migrations/991_test_hub.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
-- Insert a hub
|
||||||
|
INSERT INTO hubs (ownerId, discordGuild, name, shortDescription, longDescription, allowGiftPurchase, tos, bgmId, secretKey)
|
||||||
|
VALUES (289884287765839882, 1271619282853429330, 'Test Hub', 'A test hub', 'A test hub', TRUE, 'This Hub does not have any Terms of Service yet. If you are the Hub owner, you can update this under settings.', NULL, 'test_hub');
|
3
migrations/992_test_prod.sql
Normal file
3
migrations/992_test_prod.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
-- insert test product
|
||||||
|
INSERT INTO products (hubId, name, description, devProductID, decalId, stock, category)
|
||||||
|
VALUES ((SELECT id FROM hubs WHERE name = 'Test Hub'), 'Test Product', 'A test product', 1, NULL, -1, 'Test');
|
2879
package-lock.json
generated
Normal file
2879
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
23
package.json
Normal file
23
package.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "parcel_selfhost",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.8",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"discord.js": "^14.16.3",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"mariadb": "^3.4.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-discord": "^0.1.4",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
}
|
||||||
|
}
|
32
routes/cdn.js
Normal file
32
routes/cdn.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = global.db_pool;
|
||||||
|
const log = global.log;
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
router.get("/:fileId/:authToken", async (req, res) => {
|
||||||
|
const { fileId, authToken } = req.params;
|
||||||
|
const [auth] = await pool.query('SELECT * FROM fileAuth WHERE token = ?', [authToken]);
|
||||||
|
if (!auth) return res.status(403).send("Invalid token");
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE file = ? AND id = ?', [fileId, auth.product]);
|
||||||
|
if (!auth || !product) return res.status(404).send("File not found");
|
||||||
|
log.info("Auth and product found")
|
||||||
|
if (auth.expires < Date.now()) {
|
||||||
|
res.status(403).send("Token expired");
|
||||||
|
return pool.query('DELETE FROM fileAuth WHERE token = ?', [authToken]);
|
||||||
|
}
|
||||||
|
const safeFileId = path.basename(product.file);
|
||||||
|
const filePath = path.join(__dirname, '../productFiles', safeFileId);
|
||||||
|
if (!fs.existsSync(filePath)) return res.status(404).send("File not found");
|
||||||
|
log.info("File exists!")
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename=${product.file}.${product.fileType}`);
|
||||||
|
res.sendFile(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
log.error(`Error sending file: ${err}`);
|
||||||
|
res.status(500).send("Error sending file");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router;
|
93
routes/hub.js
Normal file
93
routes/hub.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = global.db_pool;
|
||||||
|
const log = global.log;
|
||||||
|
|
||||||
|
router.get('/getSession', async (req, res) => {
|
||||||
|
if (!req.headers.authorization) return res.status(401).json({ status: 401, message: 'Missing Authorization Header', data: {} });
|
||||||
|
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers.authorization]);
|
||||||
|
if (!hub) return res.status(401).json({ status: 401, message: 'Invalid Authorization Header', data: {} });
|
||||||
|
try {
|
||||||
|
const pid = req.query.robloxPlayerId;
|
||||||
|
if (!pid) return res.status(400).json({ status: 400, message: 'Missing Roblox Player ID' });
|
||||||
|
const [row] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [pid]);
|
||||||
|
if (!row) {
|
||||||
|
const pairingCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
await pool.query('INSERT INTO users (robloxId, pairingCode) VALUES (?, ?)', [pid, pairingCode]);
|
||||||
|
return res.status(409).json({ status: 409, message: `User is not paired! Run /link ${pairingCode} on Discord!`, pairingCode, data: {} });
|
||||||
|
} else if (!row.discordId && !row.pairingCode) {
|
||||||
|
const pairingCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
await pool.query('UPDATE users SET pairingCode = ? WHERE robloxId = ?', [pairingCode, pid]);
|
||||||
|
return res.status(409).json({ status: 409, message: `User is not paired! Run /link ${pairingCode} on Discord!`, pairingCode, data: {} });
|
||||||
|
} else if (!row.discordId && row.pairingCode !== null) {
|
||||||
|
return res.status(409).json({ status: 409, message: `User is not paired! Run /link ${row.pairingCode} on Discord!`, data: {} });
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const products = await pool.query('SELECT * FROM products WHERE hubId = ?', [hub.id]);
|
||||||
|
const purchases = await pool.query('SELECT * FROM purchases WHERE hubId = ?', [hub.id]);
|
||||||
|
// generate array of products
|
||||||
|
const respData = JSON.stringify({
|
||||||
|
status: 200,
|
||||||
|
message: "OK",
|
||||||
|
data: {
|
||||||
|
userData: {
|
||||||
|
connectedUsername: row.discordDisplayName,
|
||||||
|
},
|
||||||
|
hubData: {
|
||||||
|
id: hub.id,
|
||||||
|
name: hub.name,
|
||||||
|
description: {
|
||||||
|
shortDescription: hub.shortDescription,
|
||||||
|
longDescription: hub.longDescription,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
allowGiftPurchases: hub.allowGiftPurchase ? true : false,
|
||||||
|
},
|
||||||
|
statistics: {
|
||||||
|
totalSales: purchases.length,
|
||||||
|
},
|
||||||
|
regulatory: {
|
||||||
|
termsOfService: hub.tos
|
||||||
|
},
|
||||||
|
},
|
||||||
|
productsData: {
|
||||||
|
allProducts: products.map(product => ({
|
||||||
|
productID: product.id,
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
devproduct_id: product.devProductID,
|
||||||
|
decalID: product.decalId || "0",
|
||||||
|
stock: product.stock > 0 ? product.stock : false,
|
||||||
|
onsale: true,
|
||||||
|
category: product.category,
|
||||||
|
rating: {
|
||||||
|
currentScore: 0,
|
||||||
|
maxScore: 0,
|
||||||
|
amountOfReviews: 0
|
||||||
|
},
|
||||||
|
tags: [],
|
||||||
|
playerData: {
|
||||||
|
robloxId: pid,
|
||||||
|
ownsProduct: purchases.some(purchase => purchase.productId == product.id && purchase.robloxId == pid)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
robloxGame: {
|
||||||
|
musicId: hub.musicId,
|
||||||
|
},
|
||||||
|
requestedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}, (_, v) => typeof v === 'bigint' ? v.toString() : v)
|
||||||
|
return res.status(200).send(respData)
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
return res.status(500).json({ status: 500, message: 'Internal Server Error', data: {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
return res.status(500).json({ status: 500, message: 'Internal Server Error', data: {} });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
79
routes/payments.js
Normal file
79
routes/payments.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = global.db_pool;
|
||||||
|
|
||||||
|
|
||||||
|
// Main payment processor
|
||||||
|
router.post("/external/hub/order/complete", async (req, res) => {
|
||||||
|
// Get hub and validate secret
|
||||||
|
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers["hub-secret-key"]]);
|
||||||
|
if (!hub) return res.status(404).json({ status: "404", message: 'Invalid Authorization Header' });
|
||||||
|
|
||||||
|
// Get Roblox ID and Product ID
|
||||||
|
const { robloxID, productID } = req.body;
|
||||||
|
if (!robloxID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' });
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [robloxID]);
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]);
|
||||||
|
|
||||||
|
// Check if user and product exists
|
||||||
|
if (!user || !product) return res.status(404).json({ status: "404", message: 'User or Product not found' });
|
||||||
|
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [robloxID, product.id]);
|
||||||
|
|
||||||
|
// Check if purchase already exists
|
||||||
|
if (purchase) return res.status(200).json({ status: "200", message: 'Purchase already exists' });
|
||||||
|
|
||||||
|
// Insert purchase into database
|
||||||
|
await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [robloxID, product.id, hub.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gift validator
|
||||||
|
router.post("/external/hub/gift/validate", async (req, res) => {
|
||||||
|
// Get hub and validate secret
|
||||||
|
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers["hub-secret-key"]]);
|
||||||
|
if (!hub) return res.status(404).json({ status: "404", message: 'Invalid Authorization Header' });
|
||||||
|
|
||||||
|
// Get Roblox ID and Product ID
|
||||||
|
const { recipientID, productID } = req.body;
|
||||||
|
if (!recipientID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' });
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [recipientID]);
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]);
|
||||||
|
|
||||||
|
// Check that the product exists
|
||||||
|
if (!product) return res.status(404).json({ status: "404", message: 'Product not found', data: {} });
|
||||||
|
|
||||||
|
// Check if user exists, if not create a new user
|
||||||
|
if (!user) return res.status(404).json({ status: "404", message: 'User not found', data: {userExists: false} });
|
||||||
|
|
||||||
|
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [recipientID, product.id]);
|
||||||
|
|
||||||
|
// Check if purchase already exists
|
||||||
|
if (purchase) return res.status(409).json({ status: "409", message: 'User already owns product', data: {userExists: true, ownsProduct: true} });
|
||||||
|
|
||||||
|
// All good!
|
||||||
|
return res.status(200).json({ status: "200", message: 'User does not own product', data: {userExists: true, ownsProduct: false} });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gift processor
|
||||||
|
router.post("/external/hub/gift/complete", async (req, res) => {
|
||||||
|
// Get hub and validate secret
|
||||||
|
const [hub] = await pool.query('SELECT * FROM hubs WHERE secretKey = ?', [req.headers["hub-secret-key"]]);
|
||||||
|
if (!hub) return res.status(404).json({ status: "404", message: 'Invalid Authorization Header' });
|
||||||
|
|
||||||
|
// Get Roblox ID and Product ID
|
||||||
|
const { recipientID, productID } = req.body;
|
||||||
|
if (!recipientID || !productID) return res.status(400).json({ status: "400", message: 'Missing Roblox ID or Product ID' });
|
||||||
|
const [user] = await pool.query('SELECT * FROM users WHERE robloxId = ?', [recipientID]);
|
||||||
|
const [product] = await pool.query('SELECT * FROM products WHERE id = ?', [productID]);
|
||||||
|
|
||||||
|
// Check if user and product exists
|
||||||
|
if (!user || !product) return res.status(404).json({ status: "404", message: 'User or Product not found' });
|
||||||
|
const [purchase] = await pool.query('SELECT * FROM purchases WHERE robloxId = ? AND productId = ?', [recipientID, product.id]);
|
||||||
|
|
||||||
|
// Check if purchase already exists
|
||||||
|
if (purchase) return res.status(200).json({ status: "200", message: 'Purchase already exists' });
|
||||||
|
|
||||||
|
// Insert purchase into database
|
||||||
|
await pool.query('INSERT INTO purchases (robloxId, productId, hubId) VALUES (?, ?, ?)', [recipientID, product.id, hub.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
Loading…
Reference in a new issue