Skip to content

Commit

Permalink
docs: general facing upgrade
Browse files Browse the repository at this point in the history
remove internal events, rework public API,
add inline docs, align code standards
  • Loading branch information
HunteRoi committed Nov 16, 2022
1 parent 8a02a58 commit 379f810
Show file tree
Hide file tree
Showing 12 changed files with 419 additions and 48 deletions.
14 changes: 5 additions & 9 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Client, IntentsBitField, Partials, SlashCommandBuilder } = require('discord.js');
const { Client, IntentsBitField, SlashCommandBuilder } = require('discord.js');
const synchronizeSlashCommands = require('discord-sync-commands');
const { join } = require('path');

Expand Down Expand Up @@ -78,17 +78,13 @@ client.on('interactionCreate', async (interaction) => {
let feedback = 'Unknown command!';
switch (interaction.commandName) {
case 'code':
feedback = await manager.sendCode(
interaction.user.id,
interaction.options.getString('email', true) // !! PLEASE ADD CHECKS TO VALIDATE THE FIELD
);
feedback = await manager.sendCode(interaction.user.id, {
to: interaction.options.getString('email', true) // !! PLEASE ADD CHECKS TO VALIDATE THE FIELD
});
break;

case 'verify':
feedback = await manager.verifyCode(
interaction.user.id,
interaction.options.getString('code', true)
);
feedback = await manager.verifyCode(interaction.user.id, interaction.options.getString('code', true));
break;

// you can also add other commands such as one to change the data field for users who mistyped it in the first place
Expand Down
114 changes: 104 additions & 10 deletions src/VerificationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,46 @@ import {
} from './types';
import { VerificationManagerEvents } from './VerificationManagerEvents';

/**
* The manager handling verification (generating, sending and verifying the user's code).
*
* @export
* @class VerificationManager
* @extends {EventEmitter}
* @template TUser
*/
export class VerificationManager<TUser extends IUser> extends EventEmitter {
private readonly options: VerificationOptions<TUser>;
private readonly storingSystem: IStoringSystem<TUser>;
private readonly senderAPI: ISenderAPI;
private readonly codeGenerator: CodeGeneratorService;

/**
* The Discord client instance.
*
* @type {Client}
* @memberof VerificationManager
*/
public readonly client: Client;

/**
* Creates an instance of VerificationManager.
* @param {Client} client
* @param {IStoringSystem<TUser>} storingSystem
* @param {ISenderAPI} senderAPI
* @param {VerificationOptions<TUser>} [options={
* codeGenerationOptions: {
* length: 6
* },
* maxNbCodeCalledBeforeResend: 3,
* pendingMessage: (user: TUser) => `The verification code has just been sent to ${user.data}.`,
* alreadyPendingMessage: (user: TUser) => `You already have a verification code pending! It was sent to ${user.data}.`,
* alreadyActiveMessage: (user: TUser) => `You are already verified with the email ${user.data}!`,
* validCodeMessage: (user: TUser, validCode: string) => `Your code ${validCode} is valid! Welcome ${user.username}!`,
* invalidCodeMessage: (user: TUser, invalidCode: string) => `Your code ${invalidCode} is invalid!`
* }]
* @memberof VerificationManager
*/
constructor(
client: Client,
storingSystem: IStoringSystem<TUser>,
Expand Down Expand Up @@ -82,9 +114,17 @@ export class VerificationManager<TUser extends IUser> extends EventEmitter {
}
}

/**
* Sends the code via the communication service to the user based on the provided data.
* Saves the user, their code and their data into the database.
*
* @param {Snowflake} userid
* @param {*} data
* @return {Promise<string>} the result of the operation as a string, based on the result of the call to one of the manager's options' methods.
* @memberof VerificationManager
*/
async sendCode(userid: Snowflake, data: any): Promise<string> {
let user: TUser = (await this.storingSystem.read(userid)) ?? (await this.storingSystem.readBy((user: TUser) => user.data === data));
this.emit(VerificationManagerEvents.storingSystemCall);

if (!user) {
const discordUser = await this.client.users.fetch(userid);
Expand All @@ -105,30 +145,26 @@ export class VerificationManager<TUser extends IUser> extends EventEmitter {

await this.storingSystem.write(user);
this.emit(VerificationManagerEvents.userCreate, user);
this.emit(VerificationManagerEvents.storingSystemCall);

await this.senderAPI.send({
to: data,
...data,
name: user.username,
code,
code
});
this.emit(VerificationManagerEvents.senderCall);

this.emit(VerificationManagerEvents.userAwait, user);
return this.options.pendingMessage(user);
} else if (user && user.userid === userid && user.status === UserStatus.pending) {
user.nbCodeCalled++;
if (user.nbCodeCalled % this.options.maxNbCodeCalledBeforeResend === 0) {
await this.senderAPI.send({
to: user.data,
...user.data,
name: user.username,
code: user.code,
});
this.emit(VerificationManagerEvents.senderCall);
}

await this.storingSystem.write(user);
this.emit(VerificationManagerEvents.storingSystemCall);

this.emit(VerificationManagerEvents.userAwait, user);
return this.options.alreadyPendingMessage(user);
Expand All @@ -138,11 +174,19 @@ export class VerificationManager<TUser extends IUser> extends EventEmitter {
}
}

/**
* Verifies the provided code for the provided user.
* Increments the number of tries of that specific users.
*
* @param {string} userid
* @param {string} code
* @return {Promise<string>} the result of the operation as a string, based on the result of the call to one of the manager's options' methods.
* @memberof VerificationManager
*/
async verifyCode(userid: string, code: string): Promise<string> {
let isVerified = false;

let user: TUser = await this.storingSystem.read(userid);
this.emit(VerificationManagerEvents.storingSystemCall);

if (user) {
user.nbVerifyCalled++;
Expand All @@ -151,11 +195,11 @@ export class VerificationManager<TUser extends IUser> extends EventEmitter {
if (isVerified) {
user.status = UserStatus.active;
user.activatedCode = user.code;
user.activationTimestamp = Date.now();
user.code = null;
}

await this.storingSystem.write(user);
this.emit(VerificationManagerEvents.storingSystemCall);
}

this.emit(
Expand All @@ -171,3 +215,53 @@ export class VerificationManager<TUser extends IUser> extends EventEmitter {
: this.options.invalidCodeMessage(user, code);
}
}

/**
* Emitted when a code is generated for a user.
*
* @event VerificationManager#codeCreate
* @param {string} code
* @example
* manager.on(VerificationManagerEvents.codeCreate, (code) => {});
*/

/**
* Emitted when a code is verified for a user.
*
* @event VerificationManager#codeVerify
* @param {TUser} user
* @param {Snowflake} userid
* @param {string} code
* @param {boolean} isVerified
* @example
* manager.on(VerificationManagerEvents.codeVerify, (user, userid, code, isVerified) => {});
*/

/**
* Emitted when a user is created and stored.
*
* @event VerificationManager#userCreate
* @param {TUser} user
* @example
* manager.on(VerificationManagerEvents.userCreate, (user) => {});
*/

/**
* Emitted when a user is waiting for verification.
* Firstly emitted once the code is generated and the user is created and stored.
* Then emitted when the user tries to generate a new code while an existing one is pending.
*
* @event VerificationManager#userAwait
* @param {TUser} user
* @example
* manager.on(VerificationManagerEvents.userAwait, (user) => {});
*/

/**
* Emitted when a user is already verified.
*
* @event VerificationManager#userActive
* @param {TUser} user
* @example
* manager.on(VerificationManagerEvents.userActive, (user) => {});
*/
11 changes: 7 additions & 4 deletions src/VerificationManagerEvents.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
/**
* The events emitted by the {@link VerificationManager} on specific actions.
*
* @export
* @enum {number}
*/
export enum VerificationManagerEvents {
codeCreate = 'codeCreate',
codeVerify = 'codeVerify',

userCreate = 'userCreate',
userAwait = 'userAwait',
userActive = 'userActive',

senderCall = 'senderCall',
storingSystemCall = 'storingSystemCall',
userActive = 'userActive'
}
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { JSONDatabaseService, SendGridService } from './services';
export {
UserModel, JSONDatabaseService,
SendGridMailData, SendGridOptions, SendGridService
} from './services';
export * from './types';
export * from './VerificationManager';
export * from './VerificationManagerEvents';
30 changes: 24 additions & 6 deletions src/services/CodeGeneratorService.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
/**
* The service handling the code generation.
*
* @export
* @class CodeGeneratorService
*/
export class CodeGeneratorService {
private readonly characters: string;
readonly #characters: string;

/**
* Creates an instance of CodeGeneratorService.
* @param {string} charactersWhitelist default = '0123456789'
* @memberof CodeGeneratorService
*/
constructor(charactersWhitelist: string) {
this.characters = charactersWhitelist || '0123456789';
this.#characters = charactersWhitelist || '0123456789';
}

/**
* Generates a code of {length} characters based on the whitelist.
*
* @param {number} length
* @return {string} the code
* @memberof CodeGeneratorService
*/
generateCode(length: number): string {
if (isNaN(length)) throw new TypeError('Length must be a number');
if (length < 1) throw new RangeError('Length must be at least 1');

let code = '';
for (let i = 0; i < length; i++) {
code += this.getCharacterAtRandomPosition();
code += this.#getCharacterAtRandomPosition();
}
return code.trim();
}

private getCharacterAtRandomPosition(): string {
return this.characters.charAt(
Math.floor(Math.random() * this.characters.length)
#getCharacterAtRandomPosition(): string {
return this.#characters.charAt(
Math.floor(Math.random() * this.#characters.length)
);
}
}
36 changes: 35 additions & 1 deletion src/services/JSONDatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,65 @@ import { Config, JsonDB } from 'node-json-db';

import { IStoringSystem, IUser } from '../types';

type UserModel = IUser;
export type UserModel = IUser;

/**
* The JSON file storing system. Used to store the users.
*
* @export
* @class JSONDatabaseService
* @implements {IStoringSystem<UserModel>}
*/
export class JSONDatabaseService implements IStoringSystem<UserModel> {
readonly #database: JsonDB;
readonly #rootPath: string;

/**
* Creates an instance of JSONDatabaseService.
* @param {string} filePath
* @memberof JSONDatabaseService
*/
constructor(filePath: string) {
this.#rootPath = '/users';
this.#database = new JsonDB(new Config(filePath, true, true, '/'));
}

/**
* Creates the tables if they do not exist yet.
*
* @memberof JSONDatabaseService
*/
init() {
if (!this.#database.exists(this.#rootPath)) {
this.#database.push(this.#rootPath, []);
}
}

/**
* @inherit
* @param {Snowflake} userid
* @return {Promise<UserModel>} the user
* @memberof JSONDatabaseService
*/
read(userid: Snowflake): Promise<UserModel> {
return this.#database.find<UserModel>(this.#rootPath, (user: UserModel) => user.userid === userid);
}

/**
* @inherit
* @param {((user: IUser, index: number | string) => boolean)} callback
* @return {Promise<UserModel>} the user
* @memberof JSONDatabaseService
*/
readBy(callback: (user: IUser, index: number | string) => boolean): Promise<UserModel> {
return this.#database.find<UserModel>(this.#rootPath, callback);
}

/**
* @inherit
* @param {UserModel} user
* @memberof JSONDatabaseService
*/
async write(user: UserModel): Promise<void> {
const index = await this.#database.getIndex(this.#rootPath, user.userid, 'userid');
await this.#database.push(`${this.#rootPath}[${index !== -1 ? index.toString() : ''}]`, user, true);
Expand Down
Loading

0 comments on commit 379f810

Please sign in to comment.