Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplementing bots #119

Merged
merged 23 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
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
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
7 changes: 7 additions & 0 deletions packages/backend/src/controllers/connectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { connectorService } from './service';

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 a info message
tjarbo marked this conversation as resolved.
Show resolved Hide resolved
*
* @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 a error message
tjarbo marked this conversation as resolved.
Show resolved Hide resolved
*
* @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,11 @@
import { IConnectorDocument } from '../schemas/connector.schema';
import { IConnectorLogItemDocument } from '../schemas/connectorLogItem.schema';

export abstract class ConnectorPlugin {
public abstract send(message: string): void;
public abstract getLogs(limit: number): Promise<IConnectorLogItemDocument[]>;
public abstract update(body: { [key: string]: any }): Promise<IConnectorDocument>;
public abstract get objectId(): string;
public abstract get courses(): number[];
public abstract get isDefault(): boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Discord, { TextChannel } from 'discord.js';
import { config } from '../../../configuration/environment';
import { IConnectorDocument } from '../schemas/connector.schema';
import { ConnectorLogItem, IConnectorLogItemDocument } from '../schemas/connectorLogItem.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(private 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);
});
}

/**
* Returns an array of the newest log items of this bot.
* 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 DiscordBotConnectorPlugin
*/
public async getLogs(limit: number = 50): Promise<IConnectorLogItemDocument[]> {
const query = {
connector: this.objectId
};
return await ConnectorLogItem.find(query).sort({ createdAt: -1 }).limit(limit);
}

/**
* 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;
}

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

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

/**
* Returns true, if the bot is a default handler for unassigned courses.
*
* @readonly
* @type {boolean}
* @memberof DiscordBotConnectorPlugin
*/
public get isDefault(): boolean {
return this.document.default;
}
}
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',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* tslint:disable:ban-types */
import { Schema, model, Model, Document } from 'mongoose';
import { ConnectorType } from '../plugins';

export interface IConnectorDocument extends Document {
[_id: string]: any;
active: boolean;
courses: number[];
createdAt: Date;
default: boolean;
name: string;
socket: any;
type: ConnectorType;
}

const connectorSchema = new Schema({
active: { type: Boolean, default: true },
courses: { type: Array, default: [] },
createdAt: { type: Date, default: Date.now },
default: { type: Boolean, default: false },
name: { type: String, required: true },
socket: { type: Object },
type: { type: ConnectorType },
});

export const Connector: Model<IConnectorDocument> = model<IConnectorDocument>('Connector', connectorSchema);
Loading