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

[feature] FAQ bot add, list, and define #71

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file modified .husky/pre-commit
100644 → 100755
Empty file.
56 changes: 56 additions & 0 deletions src/bots/faq/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { dedent, escapedBackticks } from '../../utils';

/**
* Contructs string object with options
* @param {...any} options - data for strings and other options
* @return {Object.<string, string>} - string object with all FAQ bot strings
*/
export const getStrings = (...options) => {
options = options || [{}];
return {
createdFaq: `The following FAQ has been created:\n${options[0]}`,
editFaqExample: `Split values by double newline. For example:\n${options[0]}`,
faqNotFound: `FAQ with name or ID ${options[0]} not found`,
insufficientArgumentsAddFaq: 'You must supply a term and a definition',
insufficientArgumentsDefineFaq: 'You must supply a term to define',
insufficientPermissions:
'You have insufficient permissions to perform this action',
noFaqs: 'No FAQs yet :(',
successfullyDeleted: `FAQ with id ${options[0]} successfully deleted`
};
};

/**
* Possible edit fields
*/
export const possibleEditFields = dedent(
`++faq edit 5fd3f9a4ea601010fe5875ff
${escapedBackticks}term: LC

definition: LeetCode (LC) is a platform...

references: https://leetcode.com, https://en.wikipedia.org/wiki/Competitive_programming${escapedBackticks}`
);

/**
* Formats FAQ string
* @param {Object.<string, any>} faq - FAQ to be formatted
* @param {boolean} addReferences - setting to true will print reference links
* @param {boolean} addId - setting to true will print database ID
* @returns {string} - formatted FAQ string
*/
export const getFormattedFaq = (faq, addReferences, addId) => {
let details = dedent(`**${faq.term}**: ${faq.definition}`);

if (addReferences) {
details += dedent(`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe prettier do do \n instead of this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree

References: ${faq.references?.join(', ') || ''}`);
}

if (addId) {
details += dedent(`
ID: ${faq._id}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above comment

}

return details;
};
18 changes: 18 additions & 0 deletions src/bots/faq/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import addFaq from './subCommands/addFaq';
import client from '../../client';
import { commandHandler } from '../../utils';
// import deleteFaq from './subCommands/deleteFaq';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't leave comments in the code.

import defineFaq from './subCommands/defineFaq';
// import editFaq from './subCommands/editFaq';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

import { getStrings } from './constants';
import listFaqs from './subCommands/listFaqs';

const subCommands = {
add: addFaq,
define: defineFaq,
// delete: deleteFaq,
// edit: editFaq,
list: listFaqs
};

client.on('faq', () => commandHandler(subCommands, getStrings()));
14 changes: 14 additions & 0 deletions src/bots/faq/models/Faq.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import mongoose from 'mongoose';

const FaqSchema = mongoose.Schema(
{
definition: { required: true, type: String },
references: [String],
term: { required: true, type: String }
},
{ autoCreate: true, collections: 'Faq' }
);

let FaqModel = mongoose.model('Faq', FaqSchema);

export default FaqModel;
37 changes: 37 additions & 0 deletions src/bots/faq/subCommands/addFaq.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Faq from '../models/Faq';
import client from '../../../client';
import { getFormattedFaq, getStrings } from '../constants';
import { getMemberFromMessage, isMod } from '../../../utils/perms';

/**
* Handles adding an faq to Faq schema and sends message with new FAQ
* @param {Array.<string>} args - rest of command arguments
*/
const handler = async (args) => {
if (!isMod(getMemberFromMessage())) {
client.message.channel.send(getStrings().insufficientPermissions);
return;
}

if (args.length < 2) {
client.message.channel.send(getStrings().insufficientArgumentsAddFaq);
return;
}

let newFaq = await Faq({
definition: args.slice(1).join(' '),
term: args[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brendacs think that we would need to create multi word terms? Some FAQs in my mind may definitely be more than one word (examples: functional programming, data structures)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree, that was my thought from the beginning

Copy link

@payne911 payne911 Jan 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikgil what about simply using hyphens instead of spaces? Spaces are generally understood as carrying semantic within commands, so I think it's safer (for extensibility) to keep it to a "single-word".

E.g. function-programming and data-structures

}).save();

client.message.channel.send(
getStrings(getFormattedFaq(newFaq, false, false)).createdFaq
);
};

const addFaq = {
example: 'add LC LeetCode (LC) is a platform...',
handler,
usage: 'Adds new FAQ definition. Specify term then definition'
};

export default addFaq;
38 changes: 38 additions & 0 deletions src/bots/faq/subCommands/defineFaq.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import FaqModel from '../models/Faq';
import client from '../../../client';
import { getFormattedFaq, getStrings } from '../constants';

/**
* Handles getting the specified definition for a term
* @param {Array.<string>} args - rest of command arguments
*/
const handler = async (args) => {
if (args.length != 1) {
client.message.channel.send(getStrings().insufficientArgumentsDefineFaq);
return;
}

let answer;

const searchTerm = String(args[0]);
try {
answer = await FaqModel.findOne({ term: searchTerm }).exec();
} catch (_) {
client.message.channel.send(getStrings(args[0]).faqNotFound);
return;
}

if (answer) {
client.message.channel.send(getFormattedFaq(answer));
} else {
client.message.channel.send(getStrings(args[0]).faqNotFound);
}
};

const defineFaq = {
example: 'define LC',
handler,
usage: 'Specify the term you want to define'
};

export default defineFaq;
45 changes: 45 additions & 0 deletions src/bots/faq/subCommands/listFaqs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import FaqModel from '../models/Faq';
import client from '../../../client';
import { parseCommandString } from '../../../utils/index';
import { getFormattedFaq, getStrings } from '../constants';

/**
* Handles finding FAQs in the Faq schema and listing them in a message
*/
const handler = async () => {
let faqs = await FaqModel.find({}).sort({ _id: 'asc' });

if (faqs.length === 0) {
await client.message.channel.send(getStrings().noFaqs);
} else {
let parsedCmd = parseCommandString();

client.message.channel.send(
formatFaqs(faqs, parsedCmd.arguments.length > 0)
);
}
};

/**
* Formats FAQs into a readable string
* @param {Array.<Object.<string, any>>} faqs - list of faq objects
* @param {boolean} showIds - trigger that shows FAQ database IDs if set to true
* @returns {string} - message string with all FAQs
*/
const formatFaqs = (faqs, showIds) => {
let allFaqs = '**All FAQs:**\n\n';

faqs.forEach(
(faq) => (allFaqs += `${getFormattedFaq(faq, true, showIds)}\n\n`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

later problem is getting this paginated

);

return allFaqs;
};

const listFaqs = {
example: 'list [-i]',
handler,
usage: 'List all FAQs. Add -i flag to see FAQ IDs.'
};

export default listFaqs;
65 changes: 65 additions & 0 deletions src/bots/faq/subCommands/tests/addFaq.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import FaqModel from '../../models/Faq';
import { MongoMemoryServer } from 'mongodb-memory-server';
import addFaq from '../addFaq';
import client from '../../../../client';
import { dedent } from '../../../../utils';
import mongoose from 'mongoose';
import * as permUtils from '../../../../utils/perms';

describe('adding Faq', () => {
let uri;

test('faq creates an FAQ if valid information provided', async () => {
await addFaq.handler(['LC', 'LeetCode', '(LC)', 'is', 'a', 'platform']);
let results = await FaqModel.find({});
await FaqModel.deleteMany({});

expect(results.length).toEqual(1);
expect(results[0].term).toEqual('LC');
expect(results[0].definition).toEqual('LeetCode (LC) is a platform');
expect(client.message.channel.send).toHaveBeenCalledWith(
dedent(`The following FAQ has been created:
**LC**: LeetCode (LC) is a platform`)
);
});

test('faq add returns error message with insufficient permissions', async () => {
jest.spyOn(permUtils, 'isMod').mockImplementationOnce(() => false);

await addFaq.handler([]);

expect(client.message.channel.send).toHaveBeenCalledWith(
'You have insufficient permissions to perform this action'
);
});

test('faq add does not create faq with insufficient information', async () => {
await addFaq.handler(['LC']);
let results = await FaqModel.find({});

expect(results.length).toEqual(0);
expect(client.message.channel.send).toHaveBeenCalledWith(
'You must supply a term and a definition'
);
});

beforeAll(async () => {
client.message = {
channel: {
send: jest.fn()
}
};

const mongod = new MongoMemoryServer();
uri = await mongod.getUri();
});

beforeEach(async () => {
jest.spyOn(permUtils, 'isMod').mockImplementation(() => true);

await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true
});
});
});
76 changes: 76 additions & 0 deletions src/bots/faq/subCommands/tests/defineFaq.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Faq from '../../models/Faq';
import FaqModel from '../../models/Faq';
import { MongoMemoryServer } from 'mongodb-memory-server';
import client from '../../../../client';
import { dedent } from '../../../../utils';
import defineFaq from '../defineFaq';
import mongoose from 'mongoose';
import 'babel-polyfill';

describe('defining individual Faqs', () => {
let uri;

test('faq define gets the correct result', async () => {
await Faq({
definition: 'LeetCode (LC) is a platform',
references: ['https://leetcode.com/'],
term: 'LC'
}).save();

await Faq({
definition: 'Cracking the Coding Interview (CTCI) is a book',
references: ['https://leetcode.com/'],
term: 'CTCI'
}).save();

await Faq({
definition: 'You are using it!',
references: ['https://leetcode.com/'],
term: 'CSCH'
}).save();

await defineFaq.handler(['LC']);

const expectedFaqDefinition = dedent('**LC**: LeetCode (LC) is a platform');

expect(client.message.channel.send).toHaveBeenCalledWith(
expectedFaqDefinition
);
});

test('faq define sends message when no FAQs available', async () => {
await defineFaq.handler(['LC']);
expect(client.message.channel.send).toHaveBeenCalledWith(
'FAQ with name or ID LC not found'
);
});

test('faq define sends message when no arguments used', async () => {
await defineFaq.handler([]);
expect(client.message.channel.send).toHaveBeenCalledWith(
'You must supply a term to define'
);
});

beforeAll(async () => {
client.message = {
channel: {
send: jest.fn()
},

content: '++faq define'
};

const mongod = new MongoMemoryServer();
uri = await mongod.getUri();
});

beforeEach(async () => {
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true
});

await FaqModel.deleteMany({});
});
});
Loading