Skip to content

Commit

Permalink
Merge pull request #80 from Contraversum/main
Browse files Browse the repository at this point in the history
Merge main into request new match
  • Loading branch information
GregTCLTK committed Oct 22, 2023
2 parents d882553 + 96ade8d commit cee4c18
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 34 deletions.
290 changes: 274 additions & 16 deletions commands/test-command.ts
@@ -1,5 +1,6 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, SlashCommandBuilder, Guild, Role, User, TextChannel } from 'discord.js';
import { client, db } from '../common';
import { client, db } from '../../common';

Check failure on line 2 in commands/test-command.ts

View workflow job for this annotation

GitHub Actions / Build Bot (20.x)

Cannot find module '../../common' or its corresponding type declarations.
import { encrypt, decrypt } from '../../encryptionUtils';

Check failure on line 3 in commands/test-command.ts

View workflow job for this annotation

GitHub Actions / Build Bot (20.x)

Cannot find module '../../encryptionUtils' or its corresponding type declarations.
import cron from 'cron';
import 'dotenv/config';
import questions from '../questions';

Check failure on line 6 in commands/test-command.ts

View workflow job for this annotation

GitHub Actions / Build Bot (20.x)

Cannot find module '../questions' or its corresponding type declarations.
Expand Down Expand Up @@ -31,10 +32,10 @@ const checkForFeedbackRequests = async () => {
if (discordUser) {
await discordUser.send({
content: `
Hallo 👋, vor einer Woche hast du den Test ausgefüllt.
Wir können Contraversum nur durch Feedback unserer Nutzerinnen und Nutzer verbessern.
Daher wäre es ein wichtiger Beitrag für das Projekt und damit auch für die Depolarisierung
der Gesellschaft, wenn du uns Feedback geben könntest. Es dauert weniger als 3 Minuten. Vielen Dank, dein ContraBot ❤️`,
Hallo 👋, vor einer Woche hast du den Test ausgefüllt.
Wir können Contraversum nur durch Feedback unserer Nutzerinnen und Nutzer verbessern.
Daher wäre es ein wichtiger Beitrag für das Projekt und damit auch für die Depolarisierung
der Gesellschaft, wenn du uns Feedback geben könntest. Es dauert weniger als 3 Minuten. Vielen Dank, dein ContraBot ❤️`,
components: [ actionRow ]
});

Expand Down Expand Up @@ -70,8 +71,7 @@ export const sendTestButton = async () => {
const guild: Guild | undefined = client.guilds.cache.get(guildId);
if (!guild) throw new Error('Guild not found');

// (guild.channels.cache.get("1135557183845711983") as TextChannel).send({ components: [actionRow] }); // Channel Id for #How-to-basics (main server)
// (guild.channels.cache.get("1159905209414332526") as TextChannel).send({ components: [actionRow] }); // Test server channel
(guild.channels.cache.get("1135557183845711983") as TextChannel).send({ components: [ actionRow ] }); // Channel Id for #How-to-basics
};


Expand Down Expand Up @@ -133,7 +133,12 @@ export const sendQuestion = async (interaction: any) => {
const userContext = await db.db('contrabot').collection("users").findOne({ userId: interaction.user.id });

let currentQuestionIndex = userContext?.currentQuestionIndex || 0;
let userResponses = userContext?.userVector || [];
let userResponses;
if (Array.isArray(userContext?.userVector)) {
userResponses = userContext?.userVector || [];
} else {
userResponses = userContext?.userVector ? JSON.parse(decrypt(userContext.userVector)) : [];
}
var currentQuestionDisplay = currentQuestionIndex + 1

if (currentQuestionIndex === 0) {
Expand Down Expand Up @@ -166,29 +171,29 @@ export const sendQuestion = async (interaction: any) => {
components: [ builder ]
});


const encryptedUserVector = encrypt(JSON.stringify(userResponses));
// Update context for this user in the database
await db.db('contrabot').collection("users").updateOne(
{ userId: interaction.user.id },
{
$set: {
userId: interaction.user.id,
username: interaction.user.username,

currentQuestionIndex: currentQuestionIndex + 1,
userVector: userResponses,
feedbackRequestSent: false,
userVector: encryptedUserVector,
currentFeedbackQuestionIndex: 0,
invited: interaction.user.invited,
joined: interaction.user.joinedTimestamp

},
$setOnInsert: {
feedbackRequestSent: false
}
},
{ upsert: true }
);

} else {
const guildId = process.env.GUILD_ID;
if (!guildId) throw new Error('GUILD_ID not found');
initiateConversation(interaction, userResponses);

const guild: Guild | undefined = client.guilds.cache.get(guildId);

Check failure on line 198 in commands/test-command.ts

View workflow job for this annotation

GitHub Actions / Build Bot (20.x)

Cannot find name 'guildId'. Did you mean 'guild'?
if (!guild) throw new Error('Guild not found');
Expand Down Expand Up @@ -239,7 +244,6 @@ export const sendQuestion = async (interaction: any) => {
console.warn('No best match found');
interaction.user.send("Leider konnte zur Zeit kein geeigneter Gesprächspartner gefunden werden. Bitte versuchen Sie es später erneut.");
}

// Reset context for this user in the database
await db.db('contrabot').collection("users").updateOne(
{ userId: interaction.user.id },
Expand All @@ -253,6 +257,260 @@ export const sendQuestion = async (interaction: any) => {
}
}

async function initiateConversation(interaction: any, userResponses: number[]) {
const guildId = process.env.GUILD_ID;
if (!guildId) throw new Error('GUILD_ID not found');

const guild: Guild | undefined = client.guilds.cache.get(guildId);
if (!guild) throw new Error('Guild not found');

const bestMatch = await findMatchingUser(interaction.user.id, userResponses, guild);
if (!bestMatch) {
console.warn('No best match found');
interaction.user.send("Leider konnte zur Zeit kein geeigneter Gesprächspartner gefunden werden. Bitte versuchen Sie es später erneut.");
return;
}

const interactionGuildMember = guild.members.cache.get(interaction.user.id);
if (!interactionGuildMember) throw new Error('interactionGuildMember was not found');

bestMatch.GuildMember = await guild.members.fetch(bestMatch.userId);
if (!bestMatch.GuildMember) throw new Error('bestMatch.GuildMember was not found');

const matchesCategory = guild.channels.cache.find((category: any) => category.name === 'matches' && category.type === 4);
const channelName = `match-${interaction.user.username}-${bestMatch.username}`;

const textChannel = await guild.channels.create({
parent: matchesCategory?.id,
name: channelName.toLowerCase(),
type: 0,
});

await textChannel.permissionOverwrites.edit(interactionGuildMember, {
ViewChannel: true,
SendMessages: true,
});
await textChannel.permissionOverwrites.edit(bestMatch.GuildMember, {
ViewChannel: true,
SendMessages: true,
});

const everyone = await guild.roles.everyone;
await textChannel.permissionOverwrites.edit(everyone, {
ViewChannel: false,
});

await textChannel.send(`Hallo ${interactionGuildMember} 👋, hallo ${bestMatch.GuildMember} 👋, basierend auf unserem Algorithmus wurdet ihr als Gesprächspartner ausgewählt. Bitte vergesst nicht respektvoll zu bleiben. Viel Spaß bei eurem Match!`);
await textChannel.send(`Bei beispielsweise diesen drei Fragen seid ihr nicht einer Meinung:`);

// This function will send starter questions where they disagreed
conversationStarter(textChannel, interaction, bestMatch, userResponses);

interaction.user.send(`Du wurdest erfolgreich mit **@${bestMatch.username}** gematcht. Schau auf den Discord-Server um mit dem Chatten zu beginnen! 😊`);
client.users.fetch(String(bestMatch.userId)).then((user: User) => {
user.send(`Du wurdest mit **@${interaction.user.username}** gematcht. Schau auf den Discord-Server um mit dem Chatten zu beginnen! 😊`);
});

verifyUser(interaction, guild);

// Add conversation to database
const conversationInitiationTime = new Date();
await db.db('contrabot').collection('conversations').insertOne({
initiationTime: conversationInitiationTime,
interactionUserId: interaction.user.id,
bestMatchUserId: bestMatch.userId,
channelId: textChannel.id,
eightHourNotificationSent: false
});
}

async function conversationStarter(channelOfDestination: any, interaction: any, bestMatch: any, user: number[]) {
// get all contrasting and similar answers
let addedToDisagree = false; // Track if any numbers were added to disagree
const disagree: number[] = [];

user.forEach((value, i) => {
const total = value + bestMatch.userVector[ i ];
if (value !== 0 && total === 0) {
disagree.push(i);
addedToDisagree = true;
}
});
// Only add to disagree if the flag is still false
if (!addedToDisagree || disagree.length < 6) {
user.forEach((value, i) => {
const total = value + bestMatch.userVector[ i ];
if (Math.abs(total) === 1) {
disagree.push(i);
}
});
}

const selectedIndexes = getRandomDisagreement(disagree, 6);
sendDisagreedQuestions(channelOfDestination, selectedIndexes.slice(0, 3));


let bestMatchSentMessage = false;

client.on('messageCreate', (message: any) => {
if (message.channel.id === channelOfDestination.id) {
if (message.author.id === bestMatch.userId) {
bestMatchSentMessage = true;
return;
}
}
});

// send message into the channel after 8 hours if no message was sent
const eightHourCheck = new cron.CronJob('0 */8 * * *', async () => {
const conversations = await db.db('contrabot').collection('conversations').find({
channelId: channelOfDestination.id
}).toArray();

conversations.forEach(async (conv) => {

Check failure on line 370 in commands/test-command.ts

View workflow job for this annotation

GitHub Actions / Build Bot (20.x)

Parameter 'conv' implicitly has an 'any' type.
if (!bestMatchSentMessage && !conv.eightHourNotificationSent) {
await channelOfDestination.send({
embeds: [
new EmbedBuilder()
.setTitle(`👋 Hallo ${interaction.user.username}, dein Gesprächspartner hat sich noch nicht gemeldet.`)
.setDescription(`Nach 24 Stunden inactivität wirst du ein neuen Gesprächspartner erhalten.`)
.setColor('#fb2364')
]
});
// update flag in the database
await db.db('contrabot').collection('conversations').updateOne(
{ channelId: channelOfDestination.id },
{ $set: { eightHourNotificationSent: true } }
);
}
});

});
eightHourCheck.start();

const twentyFourHourCheck = new cron.CronJob('0 0 */1 * *', async () => {
const conversations = await db.db('contrabot').collection('conversations').find({
channelId: channelOfDestination.id
}).toArray();

conversations.forEach(async (conv) => {

Check failure on line 396 in commands/test-command.ts

View workflow job for this annotation

GitHub Actions / Build Bot (20.x)

Parameter 'conv' implicitly has an 'any' type.
if (!bestMatchSentMessage && conv.eightHourNotificationSent) {
//Send messages to both users
interaction.user.send(`Dein Gesprächspartner hat das Gespräch verlassen. Wir finden einen neuen Gesprächspartner für dich.`);
client.users.fetch(String(bestMatch.userId)).then((user: User) => {
user.send(`Aufgrund von Inaktivität wurde das Gespräch beendet. Bitte starte einen neuen Test, um einen neuen Gesprächspartner zu finden.`);
});

// Delete the channel, conversation and BestMatch from the database
channelOfDestination.delete();
db.db('contrabot').collection("conversations").deleteOne({ _id: conv._id });
await db.db('contrabot').collection("users").deleteOne({ userId: bestMatch.userId });
}
});
});
twentyFourHourCheck.start();
}

function getRandomDisagreement(arr: number[], num: number) {
return Array.from({ length: Math.min(num, arr.length) }, () => arr.splice(Math.floor(Math.random() * arr.length), 1)[ 0 ]);
}

function sendDisagreedQuestions(channelOfDestination: any, disagree: number[]) {
disagree.forEach((value) => {
channelOfDestination.send({
embeds: [
new EmbedBuilder()
.setTitle(`Frage: ${value + 1}/38`)
.setDescription(questions[ value ].question)
.setColor('#fb2364')
]
});
});

// Make it so that the tags of the questions are printed properly
const selectedTags = disagree
.map(index => questions[ index ].tag)
.filter(tag => tag)
.slice(0, 3);

const topicsMessage = `Als Gesprächsthemen können z.B. ${selectedTags.map(tag => `**${tag}**`).join(", ")} besprochen werden.`;
channelOfDestination.send(topicsMessage);
}

async function findMatchingUser(userId: string, userResponses: number[], guild: Guild): Promise<{ userId: string, username: string, userVector: number[], GuildMember: any } | null> {
if (!userId || !Array.isArray(userResponses) || userResponses.length === 0) {
console.log("Invalid input parameters");
return null;
}

try {
const users = await db.db('contrabot').collection("users").find({}).toArray();

if (!Array.isArray(users)) {
console.error("Error retrieving users from database");
return null;
}

let mostOppositeUser: { userId: string, username: string, userVector: number[], GuildMember: any } | null = null;
let lowestDifferenceScore = Infinity;

for (const user of users) {
if (user.userId === userId) {
console.log("Skipped: same userId as input userId");
continue;
}

let decryptedUserVector: number[]; // Explicit type declaration
if (typeof user.userVector === 'string') { // Check if it's a string
try {
decryptedUserVector = JSON.parse(decrypt(user.userVector));
} catch (error) {
console.error(`Failed to decrypt userVector for userId ${user.userId}:`, error);
continue;
}
} else {
console.warn(`Skipped: userVector for userId ${user.userId} is not a string`);
continue;
}


if (!Array.isArray(decryptedUserVector) || decryptedUserVector.length === 0) {
console.log(`Skipped: Missing or invalid decrypted userVector for userId ${user.userId}`);
continue;
}

const differenceScore = userResponses.reduce((acc, value, index) => {
return acc + value * decryptedUserVector[ index ];
}, 0);

if (differenceScore < lowestDifferenceScore) {
lowestDifferenceScore = differenceScore;
mostOppositeUser = {
userId: user.userId,
username: user.username,
userVector: decryptedUserVector,
GuildMember: null
};
}
}


if (mostOppositeUser) {
const isMember = await guild.members.fetch(mostOppositeUser.userId).then(() => true).catch(() => false);
if (!isMember) {
await db.db('contrabot').collection("users").deleteOne({ userId: mostOppositeUser.userId });
console.log(`Deleted: userId ${mostOppositeUser.userId} is no longer on the server.`);
return await findMatchingUser(userId, userResponses, guild); // Recursive call if the best match isn't a server member
}
}

return mostOppositeUser || null;

} catch (error) {
console.error("Error in findMatchingUser: ", error);
return null;
}
}
function verifyUser(interaction: any, guild: Guild) {
const role: Role | undefined = guild.roles.cache.get('1153647196449820755'); // Verified role: 1143590879274213486
if (!role) throw new Error('Role not found');
Expand Down
21 changes: 19 additions & 2 deletions documentation/Quickstart.md
Expand Up @@ -38,9 +38,26 @@ Navigate to the "Bot" tab .
Under the "TOKEN" section, click "Copy" to get your bot token.
Setting Up the Token Locally:

Create a config.json file in the root directory of the project.
Create a '.env' file in the root directory of the project.
Inside the config file, set your token like this: token:"XXX".
If you suspect your bot token has been compromised, go back to the Discord Developer Portal and regenerate the token immediately.
If you suspect your bot token has been compromised, go back to the Discord Developer Portal and regenerate the token immediately. Along with the Token you will need to set up the other secrets that can be found in the moderator channel of the Contraversum Discord server.

## Setting up for production

You will need to join the ContraversumTest Discord Server. Here you will find the production bot. You can access the production bot by creating a branch from the 'pre-production' branch and altering the .env file. With the secrets that can be found in the moderator channel of the Contraversum Discord server.

### Setting up the MongoDB database

The production bot uses a MongoDB database. You can set up a MongoDB database using the following steps:

1. Install MongoDB https://www.mongodb.com/docs/manual/administration/install-community/
2. Create a database called 'contrabot'
3. Create a collection called 'users'

### Acessing the live database

You can access the live database using the following steps:


## Running the Bot

Expand Down

0 comments on commit cee4c18

Please sign in to comment.