Skip to content

Commit

Permalink
🔀 Reimplemented bots - Merge pull request #119 from tjarbo/feature/is…
Browse files Browse the repository at this point in the history
…sue-116

Reimplementing bots
  • Loading branch information
tjarbo authored May 20, 2021
2 parents 953668e + a7bf483 commit 9b390a1
Show file tree
Hide file tree
Showing 34 changed files with 547 additions and 318 deletions.
5 changes: 5 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"coenraads.bracket-pair-colorizer-2",
"docsmsft.docs-markdown",
"oouo-diogo-perdigao.docthis",
"octref.vetur",
"benjaminadk.emojis4git",
"fabiospampinato.vscode-todo-plus"
]

// Uncomment the next line if you want start specific services in your Docker Compose config.
Expand Down
11 changes: 5 additions & 6 deletions packages/backend/src/configuration/__mocks__/environment.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
export const config = {
admin: {
id: '1234567890123456789',
name: 'testuser#00000',
},
connectorLogLifetime: 1,
discordToken: 'xxxxxxxxxxxxxx',
discordChannel: 'xxxxxxxxxxxxxx',
env: 'test',
jwt: {
secret: 'secret',
expiresIn: '6h',
},
mongo: {
host: 'mongodb://localhost:27017/fmdb',
port: 27017
},
mongooseDebug: true,
port: 4040,
moodle: {
baseURL: 'https://moodle.example.com',
fetchInterval: '12',
reminderTimeLeft: 86400,
token: 'MOODLETOKEN123',
useCourseShortname: true,
userId: 123456
},
port: 8080,
registrationTokenLifetime: 123,
rp: {
name: 'Unit Test',
id: 'localhost',
Expand Down
11 changes: 0 additions & 11 deletions packages/backend/src/configuration/discord.ts

This file was deleted.

42 changes: 23 additions & 19 deletions packages/backend/src/configuration/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,9 @@ dotenvConfig();

// define validation for all the env vars
const envVarsSchema = object({
NODE_ENV: string()
.allow('development')
.allow('production')
.allow('test')
.allow('provision')
.default('production'),
PORT: number()
.default(4040),
MONGOOSE_DEBUG: boolean()
.when('NODE_ENV', {
is: string().equal('development'),
then: boolean().default(true),
otherwise: boolean().default(false)
}),
MONGO_HOST: string()
.required()
.description('Path to your mongodb instance.'),
CONNECTOR_LOG_LIFETIME: string()
.default('31d')
.description('Defines how long log entries/items will be stored'),
DISCORD_TOKEN: string()
.required()
.description('Discord Token for bot'),
Expand All @@ -35,6 +21,15 @@ const envVarsSchema = object({
JWT_EXPIRESIN: string()
.default('10m')
.description('Defines how long a user will be logged in'),
MONGOOSE_DEBUG: boolean()
.when('NODE_ENV', {
is: string().equal('development'),
then: boolean().default(true),
otherwise: boolean().default(false)
}),
MONGO_HOST: string()
.required()
.description('Path to your mongodb instance.'),
MOODLE_BASE_URL: string()
.required()
.uri()
Expand All @@ -54,7 +49,15 @@ const envVarsSchema = object({
MOODLE_USERID: number()
.required()
.description('Moodle user Id required to fetch course details'),
REGISTRATIONTOKEN_LIFETIME: string()
NODE_ENV: string()
.allow('development')
.allow('production')
.allow('test')
.allow('provision')
.default('production'),
PORT: number()
.default(4040),
REGISTRATION_TOKEN_LIFETIME: string()
.default('15m')
.description('Defines how long a registration token can be used until it expires'),
RP_NAME: string()
Expand All @@ -79,6 +82,7 @@ const envDescriptionLink = 'https://github.com/tjarbo/discord-moodle-bot/wiki/Wh
if (error) throw new Error(`Config validation error: ${error.message} \nSee ${envDescriptionLink} for more information`);

export const config = {
connectorLogLifetime: envVars.CONNECTOR_LOG_LIFETIME,
discordToken: envVars.DISCORD_TOKEN,
discordChannel: envVars.DISCORD_CHANNEL,
env: envVars.NODE_ENV,
Expand All @@ -99,7 +103,7 @@ export const config = {
userId: envVars.MOODLE_USERID,
},
port: envVars.PORT,
registrationTokenLifetime: envVars.REGISTRATIONTOKEN_LIFETIME,
registrationTokenLifetime: envVars.REGISTRATION_TOKEN_LIFETIME,
rp: {
name: envVars.RP_NAME,
id: envVars.RP_ID,
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/configuration/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import log4js from 'log4js';
import {config} from './environment';
import { config } from './environment';

const configLogger = {
appenders: {
Expand Down
16 changes: 0 additions & 16 deletions packages/backend/src/controllers/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,6 @@ export async function authAttestationGetRequest(req: Request, res: Response, nex
userName: userDoc.username.toString(),
timeout: 60000,
attestationType: 'indirect',
/**
* Passing in a user's list of already-registered authenticator IDs here prevents users from
* registering the same device multiple times. The authenticator will simply throw an error in
* the browser if it's asked to perform an attestation when one of these ID's already resides
* on it.
*
* excludeCredentials: [{
* id: userDoc.device.credentialID,
* type: 'public-key',
* transports: userDoc.device.transports,
* }],
*/
/**
* The optional authenticatorSelection property allows for specifying more constraints around
* the types of authenticators that users can use for attestation
*/
authenticatorSelection: {
userVerification: 'preferred',
requireResidentKey: false,
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/controllers/connectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ConnectorLogType {
Info = 'info',
Warning = 'warning',
Error = 'error'
}
75 changes: 75 additions & 0 deletions packages/backend/src/controllers/connectors/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ConnectorLogType } from '.';
import { loggerFile } from '../../configuration/logger';
import { ConnectorLogItem } from './schemas/connectorLogItem.schema';

class ConnectorLogger {

/**
* Creates object with message and connector attribute for ConnectorLogItem
*
* @param message message that needs to be stored
* @param objectId objectId of the connector
* @returns object
*/
private createContent(message: string, objectId: string): { [key: string]: string } {
return {
connector: objectId,
message
};
}

/**
* Prints and stores an info message
*
* @param message message that needs to be stored
* @param objectId objectId of the connector
* @param skipSave default: true - skips adding the message to ConnectorLogs
* @returns void
*/
public info(message: string, objectId: string, skipSave: boolean = false): void {
loggerFile.info(message);

if (skipSave) return;

const content = this.createContent(message, objectId);
new ConnectorLogItem({ ...content, type: ConnectorLogType.Info }).save();
}

/**
* Prints and stores a warning message
*
* @param message message that needs to be stored
* @param objectId objectId of the connector
* @param skipSave default: true - skips adding the message to ConnectorLogs
* @returns void
*/
public warn(message: string, objectId: string, skipSave: boolean = false): void {
loggerFile.warn(message);

if (skipSave) return;

const content = this.createContent(message, objectId);
new ConnectorLogItem({ ...content, type: ConnectorLogType.Warning }).save();
}

/**
* Prints and stores an error message
*
* @param message message that needs to be stored
* @param objectId objectId of the connector
* @param skipSave default: true - skips adding the message to ConnectorLogs
* @returns void
*/
public error(message: string, objectId: string, skipSave: boolean = false): void {
loggerFile.error(message);

if (skipSave) return;

const content = this.createContent(message, objectId);
new ConnectorLogItem({ ...content, type: ConnectorLogType.Error }).save();
}
}

// This step is not required, because all functions are static
// But the usage should be similar to the loggerFile object
export const connectorLogger = new ConnectorLogger();
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { IConnectorDocument } from '../schemas/connector.schema';
import { ConnectorLogItem, IConnectorLogItemDocument } from '../schemas/connectorLogItem.schema';

export abstract class ConnectorPlugin {

protected abstract document: IConnectorDocument;

public abstract send(message: string): void;
public abstract update(body: { [key: string]: any }): Promise<IConnectorDocument>;

/**
* Returns an array of the newest log items of this connector.
* The amount of items can be limited by parameter. Default is 50.
*
* @param {number} [limit=50] Set a limit to the amount of items
* @return {Promise<IConnectorLogItemDocument[]>} Array of ConnectorLogItemDocuments
* @memberof ConnectorPlugin
*/
public async getLogs(limit: number = 50): Promise<IConnectorLogItemDocument[]> {
const query = {
connector: this.objectId
};
return await ConnectorLogItem.find(query).sort({ createdAt: -1 }).limit(limit);
}

/**
* Returns the mongoDB objectId, this connector is build on.
*
* @readonly
* @type {string}
* @memberof ConnectorPlugin
*/
public get objectId(): string { return this.document.id; }

/**
* Returns all courses, that are assigned to this bot.
*
* @readonly
* @type {{ [key: string]: string; }[]}
* @memberof ConnectorPlugin
*/
public get courses(): number[] {
return this.document.courses;
}

/**
* Returns true, if this plugin is an default handler for not
* assigned courses.
*
* @readonly
* @type {boolean}
* @memberof ConnectorPlugin
*/
public get isDefault(): boolean {
return this.document.default;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Discord, { TextChannel } from 'discord.js';
import { config } from '../../../configuration/environment';
import { IConnectorDocument } from '../schemas/connector.schema';
import { connectorLogger } from '../logger';
import { ConnectorPlugin } from './connectorPlugin.class';
import { object, ObjectSchema, string } from '@hapi/joi';
import { ApiError } from '../../../utils/api';

export class DiscordBotConnectorPlugin extends ConnectorPlugin {
private readonly client: Discord.Client = new Discord.Client();
private readonly updateRequestSchema: ObjectSchema = object({
channel: string().alphanum().length(18),
}).required();

/**
* Creates an instance of DiscordBotConnectorPlugin.
*
* @param {IConnectorDocument} document mongoose document
* @memberof DiscordBotConnectorPlugin
*/
constructor(protected document: IConnectorDocument) {
super();

this.setUpListeners();
this.client.login(config.discordToken);
}

/**
* Creates all listeners to provide better overview about the health of the bot.
*
* @private
* @memberof DiscordBotConnectorPlugin
*/
private setUpListeners(): void {
this.client.once('ready', () => {
connectorLogger.info(`Logged in as ${this.client.user.tag}!`, this.objectId);
});

this.client.on('warn', (info) => {
connectorLogger.warn(`discord.js: ${info}`, this.objectId);
});

this.client.on('disconnect', (info) => {
connectorLogger.error(`discord.js: ${info}`, this.objectId);
});
}

/**
* Sends the given message to the discord channel
*
* @param {string} message to send
*/
public send(message: string): void {
const discordChannel = this.client.channels.cache.get(this.document.socket.channel);
if (!discordChannel) return connectorLogger.error(`Channel not in discord cache. Send a small 'test' message to the channel and try again.`, this.objectId);

(discordChannel as TextChannel).send(message)
.then(() => {
connectorLogger.info('Successfully sent message via Discord bot!', this.objectId);
})
.catch((error) => {
connectorLogger.info(`Failed to send message via Discord bot! ${error.message}`, this.objectId);
});
}

/**
* Applies the given patch to the discord bot document.
*
* @param {{ [key: string]: any }} body
* @return {Promise<IConnectorDocument>} The updated document
* @memberof DiscordBotConnectorPlugin
*/
public async update(body: { [key: string]: any }): Promise<IConnectorDocument> {
// Validate user input
const updateRequest = this.updateRequestSchema.validate(body);
if (updateRequest.error) throw new ApiError(400, updateRequest.error.message);

// Apply changes
this.document.socket.channel = updateRequest.value.channel;
const result = await this.document.save();

// Log update process
connectorLogger.info('New values have been applied', this.objectId);

return result;
}
}
6 changes: 6 additions & 0 deletions packages/backend/src/controllers/connectors/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { DiscordBotConnectorPlugin } from './discordBot.class';
export { ConnectorPlugin } from './connectorPlugin.class';

export enum ConnectorType {
Discord = 'discord',
}
Loading

0 comments on commit 9b390a1

Please sign in to comment.