Skip to content
This repository has been archived by the owner on Mar 22, 2024. It is now read-only.

Discord Auth link with user, Linking of discordserver with project, Webhook and invitelink #95

Merged
merged 10 commits into from
Apr 20, 2022
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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ LDB_NAME=

# Security
SECRET_KEY=
DISCORD_BOT_TOKEN=

# Discord
DISCORD_BOT_TOKEN=
DISCORD_APPLICATION_SECRET=
DISCORD_CLIENT_ID=959004457205637131
30 changes: 30 additions & 0 deletions public/scripts/discordAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
window.onload = () => {
//console.log(window.location) //http://localhost:4000/discord?code=SUysVwZaIztTpdkvVzNe6RCZn3fRqC
const url = new URL(window.location);
const code = url.searchParams.get('code'); //SUysVwZaIztTpdkvVzNe6RCZn3fRqC
const state = url.searchParams.get('state');
//before auth
if (!code) {
const randomString = generateRandomString();
localStorage.setItem('oauth-state', randomString);

document.getElementById('login').href += `&state=${btoa(randomString)}`;
return document.getElementById('login').style.display = 'block';
}
//after auth
if (localStorage.getItem('oauth-state') !== atob(decodeURIComponent(state))) {
return document.getElementById('info').innerText = `You may have been clickjacked!`;
}
document.getElementById('info').innerText = `Discord account linked`;

function generateRandomString() {
let randomString = '';
const randomNumber = Math.floor(Math.random() * 10);

for (let i = 0; i < 20 + randomNumber; i++) {
randomString += String.fromCharCode(33 + Math.floor(Math.random() * 94));
}

return randomString;
}
}
114 changes: 114 additions & 0 deletions src/app/Http/DiscordController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
const fetch = require('node-fetch')
const path = require('path')
const User = require('../Models/User')
const Project = require('../Models/Project')

require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
const clientID = process.env.DISCORD_CLIENT_ID
const secret = process.env.DISCORD_APPLICATION_SECRET

const authRedirect = 'http://localhost:4000/discord'
const AuthLink = 'https://discord.com/api/oauth2/authorize?client_id=959004457205637131&redirect_uri=http%3A%2F%2Flocalhost%3A4000%2Fdiscord&response_type=code&scope=identify%20email'
const InviteBotLink = 'https://discord.com/api/oauth2/authorize?client_id=959004457205637131&permissions=537119937&scope=bot%20applications.commands'
const CreateServerLink = 'https://discord.new/dQCDNNwCPuhE'

const TEMP_currentproject = '624bfb0bb56cd83f0c16e346'

/**
* @function Handling of the discord service
*/
exports.discordAuth = async (req, res) => {
const code = req.query.code
if (code) {
await handleAuth(req, code)
}
let ServerInviteLink = ''
const project = await Project.findById(TEMP_currentproject)
await project
if (project.categories.messaging.services.discord) { ServerInviteLink = project.categories.messaging.services.discord.inviteLink }
res.render('projects/services/discord', {
AuthLink: AuthLink,
InviteBotLink: InviteBotLink,
CreateServerLink: CreateServerLink,
ServerInviteLink: ServerInviteLink
})
}

/**
* @function Handling of the discord authentication linking the user in session to discord
* @param req
* @param code
* @returns {Promise<void>}
*/
async function handleAuth (req, code) {
// console.log(`Discord OAuth request with code: ${req.query.code}`)
try {
const tokenResult = await getToken(code)
// console.log(tokenResult.status);
const token = await tokenResult.json()
// console.log(await token);

const userResult = await getUserData(token)
const discordUser = await userResult.json()
if ((await discordUser).message === '401: Unauthorized') {
console.log('failed to link user with discord because of invalid token')
return
}
console.log(discordUser)
// console.log(discordUser.id);
putUserInDB(discordUser, req)
} catch (error) {
console.error(error)
// TODO: add proper autherror to user
}
}

/**
* @function Uses a code from the client to fetch a token for reading the users details
* @param {int} code
* @returns {*|Promise<Response>} Token
*/
function getToken (code) {
return fetch('https://discord.com/api/oauth2/token', {
method: 'POST',
body: new URLSearchParams({
client_id: clientID,
client_secret: secret,
code: code,
grant_type: 'authorization_code',
redirect_uri: authRedirect,
scope: 'identify'
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}

/**
* @function gets userdata from discord
* @param token
* @returns {*|Promise<Response>} Userdata object
*/
function getUserData (token) {
return fetch('https://discord.com/api/users/@me', {
headers: {
authorization: `${token.token_type} ${token.access_token}`
}
})
}

/**
* @function Puts the discordUser's id in the database
* @param discordUser
* @param req
*/
function putUserInDB (discordUser, req) {
// TODO: handle the usecase where users discord is already linked to another account
const username = req.session.user.username
User.findOne({ username: username }, 'discord').then(user => {
user.discord = discordUser.id
user.save()
console.log(`Sucessfully linked ${username} with discord account ${discordUser.username} (${discordUser.id})`)
})
}
3 changes: 3 additions & 0 deletions src/app/Models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const UserSchema = new Schema({
type: Date,
default: Date.now
},
discord: {
type: String
},
HashedPassword: String,
projectIDs: [
],
Expand Down
15 changes: 8 additions & 7 deletions src/discord/CommandHandler.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
async function HandleCommand(interaction)
{
const { commandName } = interaction;
async function HandleCommand (interaction) {
const { commandName } = interaction
switch (commandName) {
case 'ping':
await interaction.reply('Pong!');
break;
case 'ping':
await interaction.reply('Pong!')
break
default:
break
}
}

module.exports.Handlecommand = HandleCommand;
module.exports.Handlecommand = HandleCommand
55 changes: 23 additions & 32 deletions src/discord/DiscordBot.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,28 @@
// Discord bot invite link: https://discord.com/api/oauth2/authorize?client_id=959004457205637131&permissions=268643377&scope=bot%20applications.commands
const path = require('path');
const { Client, Intents } = require('discord.js');
// Discord bot invite link: https://discord.com/api/oauth2/authorize?client_id=959004457205637131&permissions=537119937&scope=bot%20applications.commands
const path = require('path')
const { Client, Intents } = require('discord.js')
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
const guildCreateHandler = require('./GuildCreateHandler')

//token is the bots login credentials and needs to be kept confident
const token = process.env.DISCORD_BOT_TOKEN;
if (!token)
{
console.log("Couldn't find Discord token. Disabling Discord bot");
return;
}
//commandHandler doesn't register commands. Only handles commands when they come in from discord
const commandHandler = require('./CommandHandler');
// token is the bots login credentials and needs to be kept confident
const token = process.env.DISCORD_BOT_TOKEN
if (token) {
const client = new Client({
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_INVITES, Intents.FLAGS.GUILD_MEMBERS,
Intents.FLAGS.GUILD_PRESENCES, Intents.FLAGS.GUILD_MESSAGE_TYPING, Intents.FLAGS.GUILD_MESSAGES]
})
client.once('ready', () => {
console.log('Discord Ready!')
})
const commandHandler = require('./CommandHandler')
require('./RegCommands')
client.on('interactionCreate', async interaction => {
if (interaction.isCommand()) { await commandHandler.Handlecommand(interaction) }
// else if(interaction.is) add other interaction than commands here
})
client.on('guildCreate', guild => guildCreateHandler.OnGuildCreate(guild))
client.login(token)
} else { console.log('Couldn\'t find Discord token. Disabling Discord bot') }

// Create a new client instance (log into discord)
// Intents(intentions/what do we wanna do): //https://discord.com/developers/docs/topics/gateway#list-of-intents
const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_INVITES, Intents.FLAGS.GUILD_MEMBERS,
Intents.FLAGS.GUILD_PRESENCES, Intents.FLAGS.GUILD_MESSAGE_TYPING, Intents.FLAGS.GUILD_MESSAGES] });
client.once('ready', () => {
console.log('Discord Ready!');
});

//Register commands to the discord bot. Only needs to be done once, but nice keep commands updated
require('./RegCommands')

//Listen for the commands
client.on('interactionCreate', async interaction => {
if (interaction.isCommand())
await commandHandler.Handlecommand(interaction);
//else if(interaction.is) add other interaction than commands here

});


// Login to Discord with your client's token
client.login(token);
120 changes: 120 additions & 0 deletions src/discord/GuildCreateHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const User = require('../app/Models/User')
const Project = require('../app/Models/Project')
const { MessageActionRow, MessageButton } = require('discord.js')
// https://discord.js.org/#/docs/main/stable/class/ClientUser?scrollTo=send
async function OnGuildCreate (guild) {
// TODO: make sure that the bot has all the needed perms
const auditLogs = await guild.fetchAuditLogs() // auditLogs is the discord servers log for everything the admins do
const user = await auditLogs.entries.first().executor // We know this is the admin who added the bot
const userInDB = await User.findOne({ discord: user.id }, 'projectIDs')
await userInDB
const projects = userInDB.projectIDs
if (projects.length === 0) {
const message = `Hello thanks for adding me to ${guild.name} (id:${guild.id})\n
Unfortunately as of right now, you're not part of any projects in ProjectHub.\n
Please join a project and call (INSERT COMMAND HERE) to try again` // TODO: the command
await user.send({ content: message })
return
}
const message = `Hello thanks for adding me to ${guild.name} (id:${guild.id})\nClick to link to project:\n`

const buttonRows = await CreateButtons(projects)
// Send message with string contents and button
const sentMessage = await user.send({ content: message, components: buttonRows })
await CreateCollector(guild, sentMessage) // eventListener for when the user clicks the button
}
/**
* @function Creates buttons for the message. One button for each plausible project. A collector will later make the
* buttons do the linking.
* @param {int[]} projects
*/
async function CreateButtons (projects) {
// The buttons can max have 5 rows and 5 buttons in each row for a total of 25 buttons. NO MORE
// buttons can be added in a single message. This restriction is made by discord
const buttonRows = []
if (projects.length > 25) { console.log('Too many projects') } // TODO: handling of more than 25 projects for a user
let currentRow = new MessageActionRow()
// Each loop adds 1 button
for (let i = 0; i < projects.length; i++) {
const project = await Project.findById(projects[i]) // TODO: Error handling in case projectid is incorrect
await project
currentRow.addComponents(new MessageButton().setCustomId(`${projects[i]}`).setLabel(`${project.name}`).setStyle('PRIMARY'))
if (i % 5 === 4) {
buttonRows.push(currentRow)
currentRow = new MessageActionRow()
}
}
if (projects.length % 5 !== 4) { buttonRows.push(currentRow) }
return buttonRows
}
/**
* @function Creates a collector for the buttons. Finds the project based on the button, Creates a webhook and invitelink
* and updates the project in the database with serverid, invitelink and webhook. This is what links the project and Discord
* @param {Message<boolean>} sentMessage
* @param {guild} guild
*/
async function CreateCollector (guild, sentMessage) {
const collector = sentMessage.createMessageComponentCollector({
componentType: 'BUTTON',
maxComponents: 1
})
collector.on('collect', async messageInteraction => {
const projectID = messageInteraction.customId
const project = await Project.findById(projectID)
await messageInteraction.update({
content: `You have linked guild ${guild.id} to: ${project.name}`,
components: []
})
// TODO: handle the usecase where project is already linked or make already linked projects not display

const discord = { serverID: `${guild.id}`, Webhook: '', inviteLink: '' }
const web = await CreateWebHook(guild, discord)
const invite = await CreateInvite(guild, discord)
await web
await invite

// Update DB
project.categories.messaging.services = { ...project.categories.messaging.services, discord }
project.markModified('categories.messaging.services')
project.save()
})
}
/**
* @function Creates a discord invite
* @param {{Webhook: string}} discord
* @param {guild} guild
* @returns {Promise<Project>} Promise of Webhook written to discord.invitelink.
*/
function CreateWebHook (guild, discord) {
return guild.channels.fetch()
.then(channels => {
channels.first().createWebhook('ProjectHub Webhook', {
// Insert options like profilepic etc. here
}).then(webhook => {
webhook.send('ProjectHub Integrated')
discord.Webhook = `${webhook.id}`
// guild.fetchWebhooks().get(webhook.id); maybe need to do it with channel.fetchwebhooks instead?
})
})
.catch(console.error)
}
/**
* @function Creates a discord invite
* @param {{inviteLink: string}} discord
* @param {guild} guild
* @returns {Promise<Project>} Promise of invite written to discord.invitelink.
*/
function CreateInvite (guild, discord) {
return guild.channels.fetch()
.then(channels => {
guild.invites.create(channels.first(), {
maxAge: 0,
reason: 'Invite project members from ProjectHub'
}).then(invite => {
console.log(`Created invitelink: ${invite.url}`)
discord.inviteLink = `${invite.url}`
})
})
}

module.exports = { OnGuildCreate }
Loading