UNPKG

15.1 kBJavaScriptView Raw
1'use strict';
2
3const topics = require.main.require('./src/topics');
4const posts = require.main.require('./src/posts');
5const categories = require.main.require('./src/categories');
6const meta = require.main.require('./src/meta');
7const privileges = require.main.require('./src/privileges');
8const rewards = require.main.require('./src/rewards');
9const user = require.main.require('./src/user');
10const helpers = require.main.require('./src/controllers/helpers');
11const db = require.main.require('./src/database');
12const plugins = require.main.require('./src/plugins');
13const SocketPlugins = require.main.require('./src/socket.io/plugins');
14const pagination = require.main.require('./src/pagination');
15const social = require.main.require('./src/social');
16
17const plugin = module.exports;
18
19plugin.init = async function (params) {
20 const { router, middleware } = params;
21 const routeHelpers = require.main.require('./src/routes/helpers');
22
23 routeHelpers.setupAdminPageRoute(router, '/admin/plugins/question-and-answer', middleware, [], renderAdmin);
24 routeHelpers.setupPageRoute(router, '/unsolved', middleware, [], renderUnsolved);
25 routeHelpers.setupPageRoute(router, '/solved', middleware, [], renderSolved);
26
27 handleSocketIO();
28
29 plugin._settings = await meta.settings.get('question-and-answer');
30};
31
32plugin.appendConfig = async function (config) {
33 config['question-and-answer'] = plugin._settings;
34 return config;
35};
36
37plugin.addNavigation = async function (menu) {
38 menu = menu.concat(
39 [
40 {
41 route: '/unsolved',
42 title: '[[qanda:menu.unsolved]]',
43 iconClass: 'fa-question-circle',
44 textClass: 'visible-xs-inline',
45 text: '[[qanda:menu.unsolved]]',
46 },
47 {
48 route: '/solved',
49 title: '[[qanda:menu.solved]]',
50 iconClass: 'fa-check-circle',
51 textClass: 'visible-xs-inline',
52 text: '[[qanda:menu.solved]]',
53 },
54 ]
55 );
56 return menu;
57};
58
59plugin.addAdminNavigation = async function (header) {
60 header.plugins.push({
61 route: '/plugins/question-and-answer',
62 icon: 'fa-question-circle',
63 name: 'Q&A',
64 });
65 return header;
66};
67
68plugin.addAnswerDataToTopic = async function (hookData) {
69 if (!parseInt(hookData.templateData.isQuestion, 10)) {
70 return hookData;
71 }
72
73 hookData.templateData.icons.push(
74 parseInt(hookData.templateData.isSolved, 10) ?
75 '<span class="answered"><i class="fa fa-check"></i> [[qanda:topic_solved]]</span>' :
76 '<span class="unanswered"><i class="fa fa-question-circle"></i> [[qanda:topic_unsolved]]</span>'
77 );
78
79 return await addMetaData(hookData);
80};
81
82plugin.filterTopicGetPosts = async (hookData) => {
83 const solvedPid = parseInt(hookData.topic.solvedPid, 10);
84 const showBestAnswer = hookData.posts.length && hookData.posts[0].index === 0;
85 if (!solvedPid || !showBestAnswer) {
86 return hookData;
87 }
88 const answers = await posts.getPostsByPids([solvedPid], hookData.uid);
89 const [postsData, postSharing] = await Promise.all([
90 topics.addPostData(answers, hookData.uid),
91 social.getActivePostSharing(),
92 ]);
93 let post = postsData[0];
94 if (post) {
95 const bestAnswerTopicData = { ...hookData.topic };
96 bestAnswerTopicData.posts = postsData;
97 bestAnswerTopicData.postSharing = postSharing;
98
99 const topicPrivileges = await privileges.topics.get(hookData.topic.tid, hookData.uid);
100 await topics.modifyPostsByPrivilege(bestAnswerTopicData, topicPrivileges);
101
102 post = bestAnswerTopicData.posts[0];
103 post.index = -1;
104
105 const op = hookData.posts.shift();
106 hookData.posts.unshift(post);
107 hookData.posts.unshift(op);
108 }
109
110 hookData.posts.forEach((post) => {
111 if (post) {
112 post.isAnswer = post.pid === solvedPid;
113 }
114 });
115
116 return hookData;
117};
118
119async function addMetaData(data) {
120 const { tid } = data.templateData;
121 const { uid } = data.req;
122 const pidsToFetch = [data.templateData.mainPid, await posts.getPidsFromSet(`tid:${tid}:posts:votes`, 0, 0, true)];
123 let mainPost; let suggestedAnswer; let
124 acceptedAnswer;
125
126 if (data.templateData.solvedPid) {
127 pidsToFetch.push(data.templateData.solvedPid);
128 }
129
130 const postsData = [mainPost, suggestedAnswer, acceptedAnswer] = await posts.getPostsByPids(pidsToFetch, uid);
131 await topics.addPostData(postsData, uid);
132
133 postsData.forEach((p) => {
134 p.content = String(p.content || '')
135 .replace(/\\/g, '\\\\')
136 .replace(/\n/g, '\\n')
137 .replace(/"/g, '\\"')
138 .replace(/\t/g, '\\t');
139 });
140
141 data.templateData.mainPost = mainPost || {};
142 data.templateData.acceptedAnswer = acceptedAnswer || {};
143 if (suggestedAnswer && suggestedAnswer.pid !== data.templateData.mainPid) {
144 data.templateData.suggestedAnswer = suggestedAnswer || {};
145 }
146
147 data.res.locals.postHeader = await data.req.app.renderAsync('partials/question-and-answer/topic-jsonld', data.templateData);
148 return data;
149}
150
151plugin.getTopics = async function (hookData) {
152 hookData.topics.forEach((topic) => {
153 if (topic && parseInt(topic.isQuestion, 10)) {
154 if (parseInt(topic.isSolved, 10)) {
155 topic.icons.push('<span class="answered"><i class="fa fa-check"></i> [[qanda:topic_solved]]</span>');
156 } else {
157 topic.icons.push('<span class="unanswered"><i class="fa fa-question-circle"></i> [[qanda:topic_unsolved]]</span>');
158 }
159 }
160 });
161 return hookData;
162};
163
164plugin.filterPostGetPostSummaryByPids = async function (hookData) {
165 const tids = hookData.posts.map(p => p && p.tid);
166 const topicData = await topics.getTopicsFields(tids, ['isQuestion', 'isSolved']);
167 hookData.posts.forEach((p, index) => {
168 if (p && p.topic && topicData[index]) {
169 p.topic.isQuestion = parseInt(topicData[index].isQuestion, 10);
170 p.topic.isSolved = parseInt(topicData[index].isSolved, 10);
171 }
172 });
173 return hookData;
174};
175
176plugin.addThreadTool = async function (hookData) {
177 const isSolved = parseInt(hookData.topic.isSolved, 10);
178
179 if (parseInt(hookData.topic.isQuestion, 10)) {
180 hookData.tools = hookData.tools.concat([
181 {
182 class: `toggleSolved ${isSolved ? 'alert-warning topic-solved' : 'alert-success topic-unsolved'}`,
183 title: isSolved ? '[[qanda:thread.tool.mark_unsolved]]' : '[[qanda:thread.tool.mark_solved]]',
184 icon: isSolved ? 'fa-question-circle' : 'fa-check-circle',
185 },
186 {
187 class: 'toggleQuestionStatus',
188 title: '[[qanda:thread.tool.make_normal]]',
189 icon: 'fa-comments',
190 },
191 ]);
192 } else {
193 hookData.tools.push({
194 class: 'toggleQuestionStatus alert-warning',
195 title: '[[qanda:thread.tool.as_question]]',
196 icon: 'fa-question-circle',
197 });
198 }
199 return hookData;
200};
201
202plugin.addPostTool = async function (hookData) {
203 const data = await topics.getTopicDataByPid(hookData.pid);
204 if (!data) {
205 return hookData;
206 }
207
208 data.isSolved = parseInt(data.isSolved, 10) === 1;
209 data.isQuestion = parseInt(data.isQuestion, 10) === 1;
210 const canEdit = await privileges.topics.canEdit(data.tid, hookData.uid);
211 if (canEdit && !data.isSolved && data.isQuestion && parseInt(data.mainPid, 10) !== parseInt(hookData.pid, 10)) {
212 hookData.tools.push({
213 action: 'qanda/post-solved',
214 html: '[[qanda:post.tool.mark_correct]]',
215 icon: 'fa-check-circle',
216 });
217 }
218 return hookData;
219};
220
221plugin.getConditions = async function (conditions) {
222 conditions.push({
223 name: 'Times questions accepted',
224 condition: 'qanda/question.accepted',
225 });
226 return conditions;
227};
228
229plugin.onTopicCreate = async function (payload) {
230 let isQuestion;
231 if (payload.data.hasOwnProperty('isQuestion')) {
232 isQuestion = true;
233 }
234
235 // Overrides from ACP config
236 if (plugin._settings.forceQuestions === 'on' || plugin._settings[`defaultCid_${payload.topic.cid}`] === 'on') {
237 isQuestion = true;
238 }
239
240 if (!isQuestion) {
241 return payload;
242 }
243
244 await topics.setTopicFields(payload.topic.tid, { isQuestion: 1, isSolved: 0 });
245 await db.sortedSetAdd('topics:unsolved', Date.now(), payload.topic.tid);
246 return payload;
247};
248
249plugin.actionTopicSave = async function (hookData) {
250 if (hookData.topic && hookData.topic.isQuestion) {
251 await db.sortedSetAdd(hookData.topic.isSolved === 1 ? 'topics:solved' : 'topics:unsolved', Date.now(), hookData.topic.tid);
252 }
253};
254
255plugin.filterTopicEdit = async function (hookData) {
256 const isNowQuestion = hookData.data.isQuestion === true || parseInt(hookData.data.isQuestion, 10) === 1;
257 const wasQuestion = parseInt(await topics.getTopicField(hookData.topic.tid, 'isQuestion'), 10) === 1;
258 if (isNowQuestion !== wasQuestion) {
259 await toggleQuestionStatus(hookData.req.uid, hookData.topic.tid);
260 }
261
262 return hookData;
263};
264
265plugin.actionTopicPurge = async function (hookData) {
266 if (hookData.topic) {
267 await db.sortedSetsRemove(['topics:solved', 'topics:unsolved'], hookData.topic.tid);
268 }
269};
270
271plugin.filterComposerPush = async function (hookData) {
272 const tid = await posts.getPostField(hookData.pid, 'tid');
273 const isQuestion = await topics.getTopicField(tid, 'isQuestion');
274 hookData.isQuestion = isQuestion;
275
276 return hookData;
277};
278
279plugin.staticApiRoutes = async function ({ router, middleware, helpers }) {
280 router.get('/qna/:tid', middleware.assert.topic, async (req, res) => {
281 let { isQuestion, isSolved } = await topics.getTopicFields(req.params.tid, ['isQuestion', 'isSolved']);
282 isQuestion = isQuestion || '0';
283 isSolved = isSolved || '0';
284 helpers.formatApiResponse(200, res, { isQuestion, isSolved });
285 });
286};
287
288plugin.registerTopicEvents = async function ({ types }) {
289 types = {
290 ...types,
291 'qanda.as_question': {
292 icon: 'fa-question',
293 text: '[[qanda:thread.alert.as_question]]',
294 },
295 'qanda.make_normal': {
296 type: 'qanda.make_normal',
297 text: '[[qanda:thread.alert.make_normal]]',
298 },
299 'qanda.solved': {
300 icon: 'fa-check',
301 text: '[[qanda:thread.alert.solved]]',
302 },
303 'qanda.unsolved': {
304 icon: 'fa-question',
305 text: '[[qanda:thread.alert.unsolved]]',
306 },
307 };
308
309 return { types };
310};
311
312async function renderAdmin(req, res) {
313 const cids = await db.getSortedSetRange('categories:cid', 0, -1);
314 const data = await categories.getCategoriesFields(cids, ['cid', 'name', 'parentCid']);
315 res.render('admin/plugins/question-and-answer', {
316 categories: categories.getTree(data),
317 });
318}
319
320function handleSocketIO() {
321 SocketPlugins.QandA = {};
322
323 SocketPlugins.QandA.toggleSolved = async function (socket, data) {
324 const canEdit = await privileges.topics.canEdit(data.tid, socket.uid);
325 if (!canEdit) {
326 throw new Error('[[error:no-privileges]]');
327 }
328
329 return await toggleSolved(socket.uid, data.tid, data.pid);
330 };
331
332 SocketPlugins.QandA.toggleQuestionStatus = async function (socket, data) {
333 const canEdit = await privileges.topics.canEdit(data.tid, socket.uid);
334 if (!canEdit) {
335 throw new Error('[[error:no-privileges]]');
336 }
337
338 return await toggleQuestionStatus(socket.uid, data.tid);
339 };
340}
341
342async function toggleSolved(uid, tid, pid) {
343 let isSolved = await topics.getTopicField(tid, 'isSolved');
344 isSolved = parseInt(isSolved, 10) === 1;
345
346 const updatedTopicFields = isSolved ?
347 { isSolved: 0, solvedPid: 0 } :
348 { isSolved: 1, solvedPid: pid };
349
350 if (plugin._settings.toggleLock === 'on') {
351 updatedTopicFields.locked = isSolved ? 0 : 1;
352 }
353
354 await topics.setTopicFields(tid, updatedTopicFields);
355
356 if (isSolved) {
357 await Promise.all([
358 db.sortedSetAdd('topics:unsolved', Date.now(), tid),
359 db.sortedSetRemove('topics:solved', tid),
360 topics.events.log(tid, { type: 'qanda.unsolved', uid }),
361 ]);
362 } else {
363 await Promise.all([
364 db.sortedSetRemove('topics:unsolved', tid),
365 db.sortedSetAdd('topics:solved', Date.now(), tid),
366 topics.events.log(tid, { type: 'qanda.solved', uid }),
367 ]);
368
369 if (pid) {
370 const data = await posts.getPostData(pid);
371 await rewards.checkConditionAndRewardUser({
372 uid: data.uid,
373 condition: 'qanda/question.accepted',
374 method: async function () {
375 await user.incrementUserFieldBy(data.uid, 'qanda/question.accepted', 1);
376 },
377 });
378 }
379 }
380
381 plugins.hooks.fire('action:topic.toggleSolved', { uid: uid, tid: tid, pid: pid, isSolved: !isSolved });
382 return { isSolved: !isSolved };
383}
384
385async function toggleQuestionStatus(uid, tid) {
386 let isQuestion = await topics.getTopicField(tid, 'isQuestion');
387 isQuestion = parseInt(isQuestion, 10) === 1;
388
389 if (!isQuestion) {
390 await Promise.all([
391 topics.setTopicFields(tid, { isQuestion: 1, isSolved: 0, solvedPid: 0 }),
392 db.sortedSetAdd('topics:unsolved', Date.now(), tid),
393 db.sortedSetRemove('topics:solved', tid),
394 topics.events.log(tid, { type: 'qanda.as_question', uid }),
395 ]);
396 } else {
397 await Promise.all([
398 topics.deleteTopicFields(tid, ['isQuestion', 'isSolved', 'solvedPid']),
399 db.sortedSetsRemove(['topics:solved', 'topics:unsolved'], tid),
400 topics.events.log(tid, { type: 'qanda.make_normal', uid }),
401 ]);
402 }
403
404 plugins.hooks.fire('action:topic.toggleQuestion', { uid: uid, tid: tid, isQuestion: !isQuestion });
405 return { isQuestion: !isQuestion };
406}
407
408async function canPostTopic(uid) {
409 let cids = await categories.getAllCidsFromSet('categories:cid');
410 cids = await privileges.categories.filterCids('topics:create', cids, uid);
411 return cids.length > 0;
412}
413
414async function renderUnsolved(req, res) {
415 await renderQnAPage('unsolved', req, res);
416}
417
418async function renderSolved(req, res) {
419 await renderQnAPage('solved', req, res);
420}
421
422async function renderQnAPage(type, req, res) {
423 const page = parseInt(req.query.page, 10) || 1;
424 const { cid } = req.query;
425 const [settings, categoryData, canPost, isPrivileged] = await Promise.all([
426 user.getSettings(req.uid),
427 helpers.getSelectedCategory(cid),
428 canPostTopic(req.uid),
429 user.isPrivileged(req.uid),
430 ]);
431
432 const topicsData = await getTopics(type, page, cid, req.uid, settings);
433
434 const data = {};
435 data.topics = topicsData.topics;
436 data.showSelect = isPrivileged;
437 data.showTopicTools = isPrivileged;
438 data.allCategoriesUrl = type + helpers.buildQueryString(req.query, 'cid', '');
439 data.selectedCategory = categoryData.selectedCategory;
440 data.selectedCids = categoryData.selectedCids;
441
442 data['feeds:disableRSS'] = true;
443 const pageCount = Math.max(1, Math.ceil(topicsData.topicCount / settings.topicsPerPage));
444 data.pagination = pagination.create(page, pageCount);
445 data.canPost = canPost;
446 data.title = `[[qanda:menu.${type}]]`;
447
448 if (req.path.startsWith(`/api/${type}`) || req.path.startsWith(`/${type}`)) {
449 data.breadcrumbs = helpers.buildBreadcrumbs([{ text: `[[qanda:menu.${type}]]` }]);
450 }
451
452 res.render('recent', data);
453}
454
455async function getTopics(type, page, cids, uid, settings) {
456 cids = cids || [];
457 if (!Array.isArray(cids)) {
458 cids = [cids];
459 }
460 const set = `topics:${type}`;
461 let tids = [];
462 if (cids.length) {
463 cids = await privileges.categories.filterCids('read', cids, uid);
464 const allTids = await Promise.all(cids.map(async cid => await db.getSortedSetRevIntersect({
465 sets: [set, `cid:${cid}:tids:lastposttime`],
466 start: 0,
467 stop: 199,
468 })));
469 tids = allTids.flat().sort((tid1, tid2) => tid2 - tid1);
470 } else {
471 tids = await db.getSortedSetRevRange(set, 0, 199);
472 tids = await privileges.topics.filterTids('read', tids, uid);
473 }
474
475 const start = Math.max(0, (page - 1) * settings.topicsPerPage);
476 const stop = start + settings.topicsPerPage - 1;
477
478 const topicCount = tids.length;
479
480 tids = tids.slice(start, stop + 1);
481
482 const topicsData = await topics.getTopicsByTids(tids, uid);
483 topics.calculateTopicIndices(topicsData, start);
484 return {
485 topicCount,
486 topics: topicsData,
487 };
488}
489
\No newline at end of file