diff --git a/README.md b/README.md index 8cc0e77..5975058 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ At a minimum you need to provide the Discord bots Token, which can be found on t | DISCORD_BOT_TOKEN | Discord bots Token | NODE_ENV | What environment the bot is running in | `production`, `development` or `test` | | LOG_LEVEL | What level of logs should be displayed in console | `error`, `warn`, `info`, `verbose`, `debug` or `silly` | +| MONGODB_URI | Full connection string for MongoDB database, include db_name if user is scoped to single database | mongodb://user:password@localhost:27017/venom_db | +| MONGODB_DB_NAME | The name of the database to use for this project | venom_db | ### Bot commands diff --git a/package.json b/package.json index 180ed3e..e3b922b 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,13 @@ "discord.js": "~12.2.0", "dotenv": "~8.2.0", "inversify": "~5.0.1", + "mongodb": "~3.6.0", "reflect-metadata": "~0.1.13", "winston": "~3.3.3" }, "devDependencies": { "@types/dotenv": "~8.2.0", + "@types/mongodb": "~3.5.25", "@types/node": "~14.0.27", "@types/winston": "~2.4.4", "nodemon": "~2.0.4", @@ -35,4 +37,4 @@ "tslint-react": "~5.0.0", "typescript": "~3.9.7" } -} \ No newline at end of file +} diff --git a/src/.env.template b/src/.env.template index 515c07d..47195ab 100644 --- a/src/.env.template +++ b/src/.env.template @@ -1,4 +1,6 @@ BOT_TRIGGER=! DISCORD_BOT_TOKEN=[replace with own token] +MONGODB_URI=[mongo_connection_string] +MONGODB_DB_NAME=[mongo_db_name] NODE_ENV=production LOG_LEVEL=error \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index a10959e..eaca1a1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import container from './inversity.config'; import ConfigService from './core/services/config.service'; import LoggerService from './core/services/logger.service'; +import MongoService from './core/services/mongo.service'; import ICommand from './bot/commands/ICommand'; import rawCommands from './bot/commands'; @@ -12,10 +13,18 @@ import rawCommands from './bot/commands'; export default class App { private _configService: ConfigService = container.resolve(ConfigService); private _loggerService: LoggerService = container.resolve(LoggerService); + private _dbService: MongoService = container.resolve(MongoService); private _discordClient: Discord.Client; public async init(): Promise { + try { + await this._dbService.connect(); + } catch (err) { + this._loggerService.log('error', 'Cannot connect to database, exiting.'); + exit(1); + } + this._discordClient = new Discord.Client(); const commandList = new Discord.Collection(); @@ -44,7 +53,7 @@ export default class App { }; try { - await command.execute(message, args, prefix, commandList); + await command.execute(message, args, prefix, commandList, this._dbService); } catch (error) { this._loggerService.log('error', error.message); message.reply('there was an error trying to follow that command!'); @@ -57,11 +66,11 @@ export default class App { const greeting = greetings[Math.floor(Math.random() * greetings.length - 1)]; // favor const flavors = [ - "As PROMISED, grab a free pie! Courtesy of {random}!", - "The water is pure here! You should ask {random} for their water purified water for a sip!", - "Home of the sane, the smart and {random}!" + "As PROMISED, grab a free pie! Courtesy of {random}!", + "The water is pure here! You should ask {random} for their water purified water for a sip!", + "Home of the sane, the smart and {random}!" ]; - const randomMember = member.guild.members.cache.random(); + const randomMember = member.guild.members.cache.random(); const flavor = flavors[Math.floor(Math.random() * flavors.length - 1)]; // result member.guild.systemChannel.send(greeting.replace('{name}', member.displayName) + " " + flavor.replace('{random}', randomMember.displayName)); @@ -73,4 +82,8 @@ export default class App { exit(1); }); } + + public exit() { + this._dbService.disconnect(); + } } diff --git a/src/bot/commands/ICommand.ts b/src/bot/commands/ICommand.ts index 76ec39f..22e91ef 100644 --- a/src/bot/commands/ICommand.ts +++ b/src/bot/commands/ICommand.ts @@ -1,9 +1,10 @@ import Discord, { Collection } from 'discord.js'; +import MongoService from 'src/core/services/mongo.service'; export default interface ICommand { name: string; aliases?: string[]; description: string; example?: string; - execute: (message: Discord.Message, args: string[], prefix?: string, commands?: Collection) => Promise, + execute: (message: Discord.Message, args: string[], prefix?: string, commands?: Collection, dbService?: MongoService) => Promise, } \ No newline at end of file diff --git a/src/bot/commands/addgreeting.ts b/src/bot/commands/addgreeting.ts new file mode 100644 index 0000000..9cbb59f --- /dev/null +++ b/src/bot/commands/addgreeting.ts @@ -0,0 +1,49 @@ +import Discord, { Collection } from 'discord.js'; + +import MongoService from '../../core/services/mongo.service'; +import ConfigService from '../../core/services/config.service' + +import container from '../../inversity.config'; + +import ICommand from './ICommand'; + +const prefix = container.resolve(ConfigService).get('BOT_TRIGGER'); + +const command: ICommand = { + name: 'addgreeting', + aliases: ['ag'], + description: 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.', + example: `\`${prefix}addgreeting Welcome to the club {name}\``, + async execute(message: Discord.Message, args: string[], prefix?: string, commands?: Collection, dbService?: MongoService) { + // Only certain users can use this command + // TODO: Better handling of permissions for commands in a generic way + const permittedRoles = ['staff'] + const isPermitted = message.member.roles.cache.some(r => permittedRoles.indexOf(r.name) !== -1); + + if (!isPermitted) { + return message.author.send('Sorry but I can\'t let you add greetings!'); + } + + // Can't do much without a message + if (args.length === 0) { + return message.author.send('When adding a greeting you need to also provide a message!'); + } + + // Check for dupes + const greetingStr = args.join(' '); + const matchedMessages = await dbService.find(message.author.id, 'greetings', { message: greetingStr }); + if (matchedMessages.length > 0) { + return message.author.send('That greeting has already been added!'); + } + + const result = await dbService.insert(message.author.id, 'greetings', [{ message: greetingStr }]); + + if (!result) { + message.author.send('Uh-oh! Couldn\'t add that greeting!'); + } + + message.author.send('I\'ve added the greeting you told me about!'); + }, +}; + +export default command; \ No newline at end of file diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts index c96783d..8511997 100644 --- a/src/bot/commands/index.ts +++ b/src/bot/commands/index.ts @@ -2,10 +2,12 @@ import help from './help'; import ping from './ping'; import see from './see'; import magicball from './8ball'; +import addgreeting from './addgreeting' export default [ help, ping, see, - magicball + magicball, + addgreeting ] \ No newline at end of file diff --git a/src/core/services/config.service.ts b/src/core/services/config.service.ts index 15a75f6..96fdc38 100644 --- a/src/core/services/config.service.ts +++ b/src/core/services/config.service.ts @@ -33,6 +33,8 @@ export default class ConfigService { this.config = { BOT_TRIGGER: process.env.BOT_TRIGGER || '!', DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN, + MONGODB_URI: process.env.MONGODB_URI || '', + MONGODB_DB_NAME: process.env.MONGODB_DB_NAME || '', ENVIRONMENT: (process.env.NODE_ENV as Environment) || 'development', LOG_LEVEL: (process.env.LOG_LEVEL as LogLevel) || 'info', }; diff --git a/src/core/services/mongo.service.ts b/src/core/services/mongo.service.ts new file mode 100644 index 0000000..bb1efb2 --- /dev/null +++ b/src/core/services/mongo.service.ts @@ -0,0 +1,243 @@ +import mongodb from 'mongodb'; +import { injectable } from "inversify"; +import assert from 'assert'; + +import container from '../../inversity.config'; + +import ConfigService from './config.service'; +import LoggerService from './logger.service'; + +@injectable() +export default class MongoService { + private _configService: ConfigService = container.resolve(ConfigService); + private _loggerService: LoggerService = container.resolve(LoggerService); + + private _mongoClient: mongodb.MongoClient; + public _db: mongodb.Db; + + public async connect(): Promise { + return new Promise((resolve, reject) => { + this._mongoClient = new mongodb.MongoClient(this._configService.get('MONGODB_URI'), { useUnifiedTopology: true }); + + this._mongoClient.connect((err) => { + if (err) { + this._loggerService.log('error', 'Venom could not connect to MongoDB', err); + reject(); + } else { + this._loggerService.log('info', 'Venom is connected to MongoDB'); + + this._db = this._mongoClient.db(this._configService.get('MONGODB_DB_NAME')); + + resolve(); + } + }); + }); + } + + public disconnect() { + if (this._mongoClient && this._mongoClient.isConnected) { + this._loggerService.log('info', 'Closing connection to MongoDB'); + this._mongoClient.close(); + } + } + + /** + * Fetches the first document that matches the query + * + * @param collection String identifier for collection document belongs to + * @param query key/value pairs of search conditions + * + * @returns Returns matched document + * + * @example findOne('123456', 'collectionName', { ident: 'generated-slug' }) + */ + public async findOne(userId: string, collection: string, query: any) { + try { + this.verifyConnection(); + + const resp = await this._db.collection(collection).findOne(query); + + this._loggerService.log('verbose', 'Fetched single document from MongoDB', { + userId, + collection, + query, + resp, + }); + + return resp; + } catch (err) { + this._loggerService.log('error', 'Could not find document', { + userId, + error: err, + collection, + query, + }); + + return false; + } + } + + /** + * Fetches all documents that match the query + * + * @param collection String identifier for collection documents belong to + * @param query key/value pairs of search conditions + * + * @returns Returns matched document + * + * @example find('123456', 'collectionName', { key: 'value' }) + */ + public async find(userId: string, collection: string, query: any) { + try { + this.verifyConnection(); + + const resp = await this._db.collection(collection).find(query).toArray(); + + this._loggerService.log('verbose', 'Fetched documents matching query from MongoDB', { + userId, + collection, + query, + resp, + }); + + return resp; + } catch (err) { + this._loggerService.log('error', 'Could not find documents', { + userId, + error: err, + collection, + query, + }); + + return []; + } + } + + /** + * Adds one or more documents to given collection + * + * @param collection String identifier for collection document is to be stored again + * @param payload Array of objects containing the key/value pairs to be stored + * + * @returns Returns true for successful insert + * + * @example `insert('123456', 'collectionName', [{ body: 'This is a document!' }])` + */ + public async insert(userId: string, collection: string, payload: any[]) { + try { + this.verifyConnection(); + + const resp = await this._db.collection(collection).insert(payload); + + if (resp.result.ok !== 1 || resp.insertedCount !== payload.length) { + throw new Error(JSON.stringify(resp)); + } + + this._loggerService.log('verbose', `Inserted ${resp.insertedCount} documents into ${collection}`, { + userId, + collection, + payload, + resp + }); + + return true; + } catch (err) { + this._loggerService.log('error', 'Could not insert documents to database', { + userId, + error: err, + collection, + payload + }); + + return false; + } + } + + /** + * Update single document from MongoDB + * + * @param collection String identifier for collection documents belong to + * @param query key/value pairs of search conditions + * @param payload Objects containing the key/value pairs to be stored against existing documents + * + * @returns Returns true for successful update + * + * @example `updateMany('123456', 'collectionName', { ident: 'generated-slug' }, { secondKey: 'new data'})` + */ + public async updateMany(userId: string, collection: string, query: any, payload: any) { + try { + this.verifyConnection(); + + const resp = await this._db.collection(collection).updateMany(query, { $set: payload }); + + if (resp.result.ok !== 1) { + throw new Error(JSON.stringify(resp)); + } + + this._loggerService.log('verbose', `Updated documents into ${collection} with id ${resp.upsertedId}`, { + userId, + collection, + query, + payload, + resp + }); + + return true; + } catch (err) { + this._loggerService.log('error', 'Could not update documents', { + userId, + collection, + query, + payload, + err + }); + } + } + + /** + * Remove documents from MongoDB matching query + * + * @param collection String identifier for collection documents belong to + * @param query key/value pairs of search conditions + * + * @returns Returns true for successful delete + * + * @example `deleteMany('123456', 'collectionName', { ident: 'generated-slug' })` + */ + public async deleteMany(userId: string, collection: string, query: any) { + try { + this.verifyConnection(); + + const resp = await this._db.collection(collection).deleteMany(query); + + if (resp.result.ok !== 1) { + throw new Error(JSON.stringify(resp)); + } + + this._loggerService.log('verbose', `Deleted documents from ${collection}`, { + userId, + collection, + query, + resp + }); + + return true; + } catch (err) { + this._loggerService.log('error', 'Could not delete documents', { + userId, + collection, + query, + err + }); + + return false + } + } + + private verifyConnection() { + if (!this._mongoClient.isConnected) { + this._loggerService.log('error', 'No database connection available'); + throw new Error('No database connection available'); + } + } +} diff --git a/src/core/types/Config.ts b/src/core/types/Config.ts index 8b12d5a..d4c6bc5 100644 --- a/src/core/types/Config.ts +++ b/src/core/types/Config.ts @@ -4,6 +4,8 @@ import Environment from "./Environment"; export default interface Config { BOT_TRIGGER: string; DISCORD_BOT_TOKEN: string; + MONGODB_URI: string; + MONGODB_DB_NAME: string; ENVIRONMENT: Environment; LOG_LEVEL: LogLevel; } diff --git a/src/inversity.config.ts b/src/inversity.config.ts index 385b298..ef394f4 100644 --- a/src/inversity.config.ts +++ b/src/inversity.config.ts @@ -2,9 +2,11 @@ import { Container } from "inversify"; import ConfigService from "./core/services/config.service"; import LoggerService from "./core/services/logger.service"; +import MongoService from "./core/services/mongo.service"; const container = new Container(); container.bind(ConfigService).toSelf(); container.bind(LoggerService).toSelf(); +container.bind(MongoService).toSelf(); export default container; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 17235bd..242e972 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,25 @@ const app = new App(); try { app.init(); + + function exitHandler() { + // Cleans up the application + app.exit(); + exit(); + } + + // do something when app is closing + process.on('exit', exitHandler); + + //catches ctrl+c event + process.on('SIGINT', exitHandler); + + // catches "kill pid" (for example: nodemon restart) + process.on('SIGUSR1', exitHandler); + process.on('SIGUSR2', exitHandler); + + //catches uncaught exceptions + process.on('uncaughtException', exitHandler); } catch (e) { exit(1); } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 84de0b7..0281906 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,6 +58,13 @@ dependencies: defer-to-connect "^1.0.1" +"@types/bson@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.2.tgz#7accb85942fc39bbdb7515d4de437c04f698115f" + integrity sha512-+uWmsejEHfmSjyyM/LkrP0orfE2m5Mx9Xel4tXNeqi1ldK5XMQcDsFkBmLDtuyKUbxj2jGDo0H240fbCRJZo7Q== + dependencies: + "@types/node" "*" + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -75,7 +82,15 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/node@~14.0.27": +"@types/mongodb@~3.5.25": + version "3.5.25" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.5.25.tgz#ab187db04d79f8e3f15af236327dc9139d9d4736" + integrity sha512-2H/Owt+pHCl9YmBOYnXc3VdnxejJEjVdH+QCWL5ZAfPehEn3evygKBX3/vKRv7aTwfNbUd0E5vjJdQklH/9a6w== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/node@*", "@types/node@~14.0.27": version "14.0.27" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== @@ -171,6 +186,14 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== +bl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493" + integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + boxen@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" @@ -200,6 +223,11 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" +bson@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.5.tgz#2aaae98fcdf6750c0848b0cba1ddec3c73060a34" + integrity sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg== + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -405,6 +433,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +denque@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -801,6 +834,11 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -837,6 +875,19 @@ mkdirp@^0.5.3: dependencies: minimist "^1.2.5" +mongodb@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.0.tgz#babd7172ec717e2ed3f85e079b3f1aa29dce4724" + integrity sha512-/XWWub1mHZVoqEsUppE0GV7u9kanLvHxho6EvBxQbShXTKYF9trhZC2NzbulRGeG7xMJHD8IOWRcdKx5LPjAjQ== + dependencies: + bl "^2.2.0" + bson "^1.1.4" + denque "^1.4.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -979,7 +1030,7 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.3.7: +readable-stream@^2.3.5, readable-stream@^2.3.7: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -1027,6 +1078,19 @@ registry-url@^5.0.0: dependencies: rc "^1.2.8" +require_optional@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + resolve@^1.3.2: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" @@ -1041,15 +1105,22 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" +safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" semver-diff@^3.1.1: version "3.1.1" @@ -1058,7 +1129,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver@^5.3.0, semver@^5.7.1: +semver@^5.1.0, semver@^5.3.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -1098,6 +1169,13 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"