From 7f20ec033675394a64769f2a55d3bcbdcbeb9c9e Mon Sep 17 00:00:00 2001 From: dlinux-host Date: Thu, 3 Oct 2024 12:22:56 -0400 Subject: [PATCH] first commit --- .gitignore | 3 + README.md | 130 +++++++++++++++++++++++++++++++++++++++++++ models/Url.js | 15 +++++ package.json | 20 +++++++ short-backend-api.js | 94 +++++++++++++++++++++++++++++++ short.js | 100 +++++++++++++++++++++++++++++++++ 6 files changed, 362 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 models/Url.js create mode 100644 package.json create mode 100644 short-backend-api.js create mode 100644 short.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..941d536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +node_modules +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3029705 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# URL Shortener Bot with Backend API + +This repository contains two components: a **Discord bot** for shortening URLs and a **backend API** for handling the URL shortening and redirection. Both parts work together to provide a seamless experience for shortening URLs and sharing them directly from Discord. + +## Features + +- **Discord Slash Command** for URL shortening with domain selection. +- **Express.js Backend API** to handle the actual shortening process and redirection. +- **MongoDB Integration** for storing and retrieving shortened URLs. +- **API Key Validation** for secure access to the backend. + +## Components + +### 1. `short.js` - Discord URL Shortener Bot + +This is a Discord bot built using `discord.js` that provides a command to shorten URLs directly from Discord. Users can choose from multiple domains, or the bot will use a default domain. + +#### Setup Instructions for `short.js` + +1. **Install dependencies**: + ```bash + npm install discord.js unirest dotenv + ``` + +2. **Environment Variables**: + Create a `.env` file in the project root and add: + ``` + BOT_TOKEN=your_discord_bot_token + DISCORD_CLIENT_ID=your_discord_client_id + API_KEY=your_url_shortening_api_key + ``` + +3. **Run the bot**: + ```bash + node short.js + ``` + +#### How It Works + +- Users enter a URL and optionally choose a domain using the `/shortenurl` slash command. +- The bot validates the URL and sends a request to the backend API to generate the shortened link. +- The response is sent back as a Discord embed message with the shortened URL. + +### 2. `short-backend-api.js` - URL Shortener Backend + +This is a Node.js backend API using `Express.js` and `MongoDB` for URL shortening. It receives requests from the Discord bot or any client and provides a short URL in response. + +#### Setup Instructions for `short-backend-api.js` + +1. **Install dependencies**: + ```bash + npm install express mongoose shortid dotenv + ``` + +2. **MongoDB Setup**: + Make sure MongoDB is running and accessible. By default, it connects to `mongodb://127.0.0.1:27017/shorturl`. + +3. **Environment Variables**: + Create a `.env` file in the project root with: + ``` + API_KEY=your_secure_api_key + ``` + +4. **Run the API**: + ```bash + node short-backend-api.js + ``` + +#### Routes + +- **POST `/api/shorturl`**: Shortens a URL. + - **Request**: + - Body: `{ "longUrl": "https://example.com", "domain": "s.shells.lol" }` + - Header: `x-api-key: your_api_key` + - **Response**: `{ "shortUrl": "https://s.shells.lol/abcd1234" }` +- **GET `/:shortId`**: Redirects to the original long URL. + +#### How It Works + +- The API checks if the provided API key is valid. +- The `longUrl` is checked against the database. If it has already been shortened, the existing short URL is returned. +- If it's a new URL, it generates a unique `shortId` using `shortid` and stores it in the database. +- The `shortId` can then be used to redirect users to the original URL when they visit `https://yourdomain.com/:shortId`. + +## Models + +### `Url` Model + +This model represents the schema for storing URLs in MongoDB. It consists of the following fields: + +- **longUrl**: The original URL. +- **shortId**: The shortened identifier for the URL. + +```javascript +const mongoose = require('mongoose'); + +const urlSchema = new mongoose.Schema({ + longUrl: { type: String, required: true }, + shortId: { type: String, required: true } +}); + +module.exports = mongoose.model('Url', urlSchema); +``` + +## Full Setup + +### Prerequisites + +- [Node.js](https://nodejs.org/) v16.6.0 or higher +- [MongoDB](https://www.mongodb.com/) running locally or remotely +- A valid Discord bot token and a URL shortening API key + +### Steps + +1. Clone the repository: + ```bash + git clone https://github.com/yourusername/url-shortener-bot.git + cd url-shortener-bot + ``` + +2. Follow the setup instructions for both `short.js` and `short-backend-api.js`. + +3. Ensure the bot and backend API are both running: + ```bash + # In one terminal, run the backend API + node short-backend-api.js + + # In another terminal, run the Discord bot + node short.js + ``` diff --git a/models/Url.js b/models/Url.js new file mode 100644 index 0000000..0e3570b --- /dev/null +++ b/models/Url.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); + +const urlSchema = new mongoose.Schema({ + longUrl: { + type: String, + required: true, + }, + shortId: { + type: String, + required: true, + unique: true, + }, +}); + +module.exports = mongoose.model('Url', urlSchema); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d6fd6a2 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "short", + "version": "1.0.0", + "main": "short.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.7", + "discord.js": "^14.16.3", + "dotenv": "^16.4.5", + "unirest": "^0.6.0", + "express": "^4.18.2", + "mongoose": "^7.0.4", + "shortid": "^2.2.16" + } +} diff --git a/short-backend-api.js b/short-backend-api.js new file mode 100644 index 0000000..bbfedad --- /dev/null +++ b/short-backend-api.js @@ -0,0 +1,94 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const shortid = require('shortid'); +const Url = require('./models/Url'); +require('dotenv').config(); + +const app = express(); +const port = 9043; + +// Middleware to parse JSON +app.use(express.json()); + +// MongoDB connection +mongoose.connect('mongodb://127.0.0.1:27017/shorturl'); + +const db = mongoose.connection; +db.on('error', console.error.bind(console, 'connection error:')); +db.once('open', () => { + console.log('Connected to MongoDB'); +}); + +// Supported domains +const supportedDomains = [ + 's.shells.lol', + 's.hehe.rest', + 's.dcord.rest', // default domain + 's.nodejs.lol', + 's.dht.rest', + 's.tcp.quest' +]; + +// Middleware to check API key +const validateApiKey = (req, res, next) => { + const apiKey = req.headers['x-api-key']; + if (apiKey && apiKey === process.env.API_KEY) { + next(); + } else { + res.status(403).json({ error: 'Forbidden' }); + } +}; + +// Route to create a short URL +app.post('/api/shorturl', validateApiKey, async (req, res) => { + const { longUrl, domain } = req.body; + + if (!longUrl) { + return res.status(400).json({ error: 'Invalid URL' }); + } + + // Validate domain, default to 's.dcord.rest' if not provided or invalid + const selectedDomain = supportedDomains.includes(domain) ? domain : 's.dcord.rest'; + + try { + let url = await Url.findOne({ longUrl }); + + if (url) { + return res.json({ shortUrl: `https://${selectedDomain}/${url.shortId}` }); + } + + const shortId = shortid.generate(); + url = new Url({ + longUrl, + shortId, + }); + + await url.save(); + res.json({ shortUrl: `https://${selectedDomain}/${shortId}` }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Route to handle redirect +app.get('/:shortId', async (req, res) => { + const { shortId } = req.params; + + try { + const url = await Url.findOne({ shortId }); + + if (url) { + return res.redirect(301, url.longUrl); + } + + res.status(404).json({ error: 'URL not found' }); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'Server error' }); + } +}); + +app.listen(port, () => { + console.log(`Server running on port ${port}`); +}); \ No newline at end of file diff --git a/short.js b/short.js new file mode 100644 index 0000000..9c9af25 --- /dev/null +++ b/short.js @@ -0,0 +1,100 @@ +const { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder, EmbedBuilder } = require('discord.js'); +const unirest = require('unirest'); +require('dotenv').config(); + +const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN); +const DiscordClientId = process.env.DISCORD_CLIENT_ID; // Your Discord Client ID + +// URL validation function +function isValidURL(url) { + const regex = /^(https?:\/\/)?([a-zA-Z0-9\-\.]+)\.([a-z]{2,})(\/\S*)?$/; + return regex.test(url); +} + +client.once('ready', async () => { + console.log(`Logged in as ${client.user.tag}`); + + // Define your slash commands using SlashCommandBuilder + const command = new SlashCommandBuilder() + .setName('shortenurl') + .setDescription('Shorten a URL') + .addStringOption(option => + option.setName('url') + .setDescription('Enter the URL to shorten') + .setRequired(true)) + .addStringOption(option => + option.setName('domain') + .setDescription('Choose the domain for the short URL') + .setRequired(false) + .addChoices( + { name: 's.shells.lol', value: 's.shells.lol' }, + { name: 's.hehe.rest', value: 's.hehe.rest' }, + { name: 's.dcord.rest', value: 's.dcord.rest' }, // default domain + { name: 's.nodejs.lol', value: 's.nodejs.lol' }, + { name: 's.dht.rest', value: 's.dht.rest' }, + { name: 's.tcp.quest', value: 's.tcp.quest' } + ) + ) + .toJSON(); // Convert to JSON + + // Add extras to the command for integration + const extras = { + "integration_types": [0, 1], // 0 for guild, 1 for user + "contexts": [0, 1, 2], // 0 for guild, 1 for app DMs, 2 for GDMs and other DMs + }; + + Object.keys(extras).forEach(key => command[key] = extras[key]); + + try { + // Register the command with Discord + await rest.put(Routes.applicationCommands(DiscordClientId), { body: [command] }); + console.log('Commands registered successfully.'); + } catch (error) { + console.error('Error registering commands:', error); + } +}); + +client.on('interactionCreate', async interaction => { + if (!interaction.isCommand() || interaction.commandName !== 'shortenurl') return; + + const domain = interaction.options.getString('domain') || 's.dcord.rest'; + const url = interaction.options.getString('url'); + + // Validate the URL format before proceeding + if (!isValidURL(url)) { + return interaction.reply({ content: 'Please provide a valid URL.', ephemeral: true }); + } + + try { + // Make API call to the selected domain to create a short URL + unirest.post(`https://${domain}/api/shorturl`) + .headers({ + 'Content-Type': 'application/json', + 'x-api-key': process.env.API_KEY // Use the API key from environment variables + }) + .send({ longUrl: url, domain }) + .end((response) => { + const data = response.body; + + if (data.shortUrl) { + // Render the short URL directly without Markdown brackets for proper embedding + const embed = new EmbedBuilder() + .setColor("#FF0000") + .setTitle("🔗 URL Shortened!") + .setDescription(`${data.shortUrl}`) + .setTimestamp() + .setFooter({ text: `Requested by ${interaction.user.tag}`, iconURL: `${interaction.user.displayAvatarURL()}` }); + + interaction.reply({ embeds: [embed] }); + } else { + interaction.reply({ content: `Failed to shorten URL: ${data.error || 'Unknown error'}`, ephemeral: true }); + } + }); + } catch (error) { + console.error('Error creating shortened URL:', error); + interaction.reply({ content: 'There was an error trying to shorten the URL. Please try again later.', ephemeral: true }); + } +}); + +client.login(process.env.BOT_TOKEN);