-
Notifications
You must be signed in to change notification settings - Fork 8
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
Changes from all commits
21b30ee
eb5c035
127df09
ee2143b
9dfe6d5
7f1998a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(` | ||
References: ${faq.references?.join(', ') || ''}`); | ||
} | ||
|
||
if (addId) { | ||
details += dedent(` | ||
ID: ${faq._id}`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above comment |
||
} | ||
|
||
return details; | ||
}; |
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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())); |
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; |
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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i agree, that was my thought from the beginning There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
}).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; |
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; |
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`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
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 | ||
}); | ||
}); | ||
}); |
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({}); | ||
}); | ||
}); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree