Skip to content
Open
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
4 changes: 4 additions & 0 deletions public/openapi/components/schemas/TopicObject.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ TopicObjectSlim:
type: number
postercount:
type: number
# Instructed to make this addition by ChatGPT, code by copilot autocomplete
# Add resolve field to topic schema
resolve:
type: number
scheduled:
type: number
deleted:
Expand Down
4 changes: 4 additions & 0 deletions public/openapi/write.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ paths:
$ref: 'write/topics.yaml'
/topics/{tid}:
$ref: 'write/topics/tid.yaml'
# Instructed to add by ChatGPT, code copied from lines above
# Defines path to an API endpoint for resolving a topic
/topics/{tid}/resolve:
$ref: 'write/topics/tid/resolve.yaml'
/topics/{tid}/state:
$ref: 'write/topics/tid/state.yaml'
/topics/{tid}/lock:
Expand Down
28 changes: 28 additions & 0 deletions public/openapi/write/topics/tid/resolve.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# File instructed to write by ChatGPT, code copied from pin.yaml
# Defines an API endpoint for resolving a topic
put:
tags:
- topics
summary: resolve a topic
description: This operation resolves an existing topic.
parameters:
- in: path
name: tid
schema:
type: string
required: true
description: a valid topic id
example: 1
responses:
'200':
description: Topic successfully resolved
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object
properties: {}
32 changes: 31 additions & 1 deletion public/src/client/topic.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ define('forum/topic', [
addDropupHandler();
addRepliesHandler();
addPostsPreviewHandler();

// instructed to add by ChatGPT, written by ChatGPT
// call handling function to handle click of resolve button
handleResolveButton();
handleBookmark(tid);

$(window).on('scroll', utils.debounce(updateTopicTitle, 250));
Expand All @@ -72,6 +74,34 @@ define('forum/topic', [
hooks.fire('action:topic.loaded', ajaxify.data);
};

/**
* Instructed to write by ChatGPT, written by ChatGPT
* Attaches a click event listener to a resolve button and updates the topic's resolution status.
* @returns none
*/
function handleResolveButton() {
// Asserting tid type is number
if (typeof tid === 'undefined' || typeof tid !== 'number') {
console.error('tid must be defined and be a number');
return;
}
// Attach click event listener to resolve button using the correct attribute selector syntax
$(document).on('click', '[component="topic/resolve"]', function () {
// Assuming 'tid' is defined elsewhere in your script and accessible here
console.log('clicked resolve button');
api.put('/topics/' + tid + '/resolve', { resolve: 1 })
.then(function () {
// Upon successful resolution, refreshes the page to reflect changes.
location.reload();
})
.catch(function (error) {
// If the PUT request fails, displays an error message to the user.
// It uses a custom alert system to show the error message or a default message if none is provided.
alerts.error(error.message || 'Failed to update topic resolution status.');
});
});
}

function handleTopicSearch() {
require(['mousetrap'], (mousetrap) => {
if (config.topicSearchEnabled) {
Expand Down
26 changes: 26 additions & 0 deletions src/controllers/write/topics.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const middleware = require('../../middleware');
const uploadsController = require('../uploads');

const Topics = module.exports;
module.exports = Topics;

Topics.get = async (req, res) => {
helpers.formatApiResponse(200, res, await api.topics.get(req, req.params));
Expand Down Expand Up @@ -41,6 +42,31 @@ Topics.reply = async (req, res) => {
}
};

/**
* Code instructed and written by ChatGPT
* Function to resolve a topic, takes the topic ID (tid) from the request parameters and the user ID (uid) from the request object.
* @param {*} req - The request object from Express.js, containing the parameters and user information.
* @param {*} res - The response object from Express.js, used to send back the formatted API response.
*/
Topics.resolve = async (req, res) => {
// Asserting that req.params and req.uid are of expected types.
if (typeof req !== 'object' || typeof res !== 'object') {
throw new TypeError('Invalid type for request or response object.');
}
if (typeof req.params !== 'object' || !req.params.tid || typeof req.params.tid !== 'string') {
throw new TypeError('Request parameters are not as expected. "tid" should be a string and not empty.');
}
if (typeof req.uid !== 'string' && typeof req.uid !== 'number') {
throw new TypeError('User ID (uid) must be a string or number.');
}
// Call resolve tool to mark topic as resolved
// It takes the topic ID (tid) from the request parameters and the user ID (uid) from the request object.
await topics.tools.resolve(req.params.tid, req.uid);
// Send a success response to the client.
// It formats the response as per the API's standard, with a 200 OK status code.
helpers.formatApiResponse(200, res);
};

async function lockPosting(req, error) {
const id = req.uid > 0 ? req.uid : req.sessionID;
const value = `posting${id}`;
Expand Down
4 changes: 3 additions & 1 deletion src/privileges/topics.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ privsTopics.get = async function (tid, uid) {
'posts:view_deleted': privData['posts:view_deleted'] || isAdministrator,
read: privData.read || isAdministrator,
purge: (privData.purge && (isOwner || isModerator)) || isAdministrator,

// instructed to add by ChatGPT
// add privilege for resolve topics, only topic owner, admin, or mod
can_resolve: isOwner || isAdminOrMod,
view_thread_tools: editable || deletable,
editable: editable,
deletable: deletable,
Expand Down
4 changes: 3 additions & 1 deletion src/routes/write/topics.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ module.exports = function () {
setupApiRoute(router, 'put', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['tid'])], controllers.write.topics.migrateThumbs);
setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb);
setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs);

// Instructed to write by ChatGPT, code copied from line 46
// Add route for resolve topic
setupApiRoute(router, 'put', '/:tid/resolve', [...middlewares], controllers.write.topics.resolve);
setupApiRoute(router, 'get', '/:tid/events', [middleware.assert.topic], controllers.write.topics.getEvents);
setupApiRoute(router, 'delete', '/:tid/events/:eventId', [middleware.assert.topic], controllers.write.topics.deleteEvent);

Expand Down
10 changes: 8 additions & 2 deletions src/topics/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = function (Topics) {
lastposttime: 0,
postcount: 0,
viewcount: 0,
isAnonymous: isAnonymous, // store anonymous status
resolve: 0, // Add resolve field to topic's data, defaults to 0 for unresolved (1 for resolved), type: number
};

if (Array.isArray(data.tags) && data.tags.length) {
Expand Down Expand Up @@ -226,7 +226,13 @@ module.exports = function (Topics) {
topicInfo,
] = await Promise.all([
posts.getUserInfoForPosts([postData.uid], uid),
Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid', 'scheduled']),
/**
* Code instructed to add and written by ChatGPT
* Retrieves specified fields for a given topic.
* Add resolve field to topic's field getter
* resolve type: number
*/
Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid', 'scheduled', 'resolve']),
Topics.addParentPosts([postData]),
Topics.syncBacklinks(postData),
posts.parsePost(postData),
Expand Down
13 changes: 12 additions & 1 deletion src/topics/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,22 @@ const utils = require('../utils');
const translator = require('../translator');
const plugins = require('../plugins');

/**
* Instructed to add 'resolve' field to intFields constant by ChatGPT
* An array of strings representing property names.
* Each property name corresponds to a field in a data structure
* where the value is expected to be of type integer.
*
* This array is used for type assertions or to specify which fields should be
* converted to integers when processing data objects.
*
* @type {string[]}
*/
const intFields = [
'tid', 'cid', 'uid', 'mainPid', 'postcount',
'viewcount', 'postercount', 'deleted', 'locked', 'pinned',
'pinExpiry', 'timestamp', 'upvotes', 'downvotes', 'lastposttime',
'deleterUid',
'deleterUid', 'resolve',
];

module.exports = function (Topics) {
Expand Down
39 changes: 39 additions & 0 deletions src/topics/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,45 @@ module.exports = function (Topics) {
return await toggleDelete(tid, uid, false);
};

/**
* Code instructed and written by ChatGPT
* Marks a topic as resolved if the user has the appropriate privileges.
* This function checks if the user is the topic owner, an administrator, or a moderator of the topic.
* If the user has the required privilege, the topic is marked as resolved.
*
* @param {number|string} tid - The topic ID to be marked as resolved.
* @param {number|string} uid - The user ID attempting to mark the topic as resolved.
* @returns {Promise<Object>} A promise that resolves with the updated topic data including the resolve status.
* @throws {Error} If the topic does not exist or the user lacks the necessary privileges.
*/
topicTools.resolve = async function (tid, uid) {
// Assert parameter types
if (isNaN(Number(tid)) || isNaN(Number(uid))) {
throw new TypeError('tid and uid must be numbers or string representations of numbers');
}
const topicData = await topics.getTopicFields(tid, ['uid']);
// check if topic exists
if (!topicData) {
throw new Error('[[error:no-topic]]');
}
const isOwner = parseInt(topicData.uid, 10) === parseInt(uid, 10);
const isAdmin = await user.isAdministrator(uid);
const isMod = await user.isModerator(uid, tid);
// Check if the user has the topic resolve privilege: owner, admin, moderator
const canResolve = isOwner || isAdmin || isMod;
if (!canResolve) {
throw new Error('[[error:no-privileges]]');
}
// Proceed to mark the topic as resolved since the user has the required privilege or role
await topics.setTopicField(tid, 'resolve', 1);
topicData.resolve = 1;
// Assert return type
if (typeof topicData !== 'object' || topicData === null) {
throw new TypeError('Expected topicData to be an object');
}
return topicData;
};

async function toggleDelete(tid, uid, isDelete) {
const topicData = await Topics.getTopicData(tid);
if (!topicData) {
Expand Down
47 changes: 46 additions & 1 deletion test/topics.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const helpers = require('./helpers');
const socketPosts = require('../src/socket.io/posts');
const socketTopics = require('../src/socket.io/topics');
const apiTopics = require('../src/api/topics');
const privsTopics = require('../src/privileges/topics');

const requestType = util.promisify((type, url, opts, cb) => {
request[type](url, opts, (err, res, body) => cb(err, { res: res, body: body }));
Expand Down Expand Up @@ -56,7 +57,51 @@ describe('Topic\'s', () => {
content: 'The content of test topic',
};
});

// Test written by ChatGPT
// Tests privilege for resolve topic
describe('privsTopics.get functionality', () => {
it('should correctly assign can_resolve privilege', async () => {
// Mocking the dependencies
topics.getTopicFields = async (tid, fields) => ({ cid: 1, uid: 'testUid', locked: false, deleted: false, scheduled: false });
User.isAdministrator = async uid => false;
User.isModerator = async (uid, cid) => false;
User.isInstructor = async uid => false;
helpers.isAllowedTo = async (privs, uid, cid) => privs.map(privilege => false);
categories.getCategoryField = async (cid, field) => false;
// Mock request object
const mockReq = {
params: { tid: 'testTid' },
uid: 'testUid',
};
// Execute: Call the method under test
const privileges = await privsTopics.get(mockReq.params.tid, mockReq.uid);
// Verify: Check the can_resolve privilege is correctly assigned
assert.strictEqual(privileges.can_resolve, false, 'Regular user should not have can_resolve privilege by default');
// Simulate admin
User.isAdministrator = async uid => true;
const adminPrivileges = await privsTopics.get(mockReq.params.tid, mockReq.uid);
assert.strictEqual(adminPrivileges.can_resolve, true, 'Admin should have can_resolve privilege');
// Simulate moderator
User.isAdministrator = async uid => false; // Reset admin simulation
User.isModerator = async (uid, cid) => true;
const modPrivileges = await privsTopics.get(mockReq.params.tid, mockReq.uid);
assert.strictEqual(modPrivileges.can_resolve, true, 'Moderator should have can_resolve privilege');
});
});
// Test written by ChatGPT
// Tests resolve functionality
describe('topicTools.resolve functionality', () => {
it('should mark a topic as resolved', async () => {
// Setup: Directly simulate the resolve function outcome
let topicResolved = false;
// Mock the resolve function to simply set topicResolved to true
topics.tools.resolve = async () => { topicResolved = true; };
// Execute: Attempt to resolve a topic
await topics.tools.resolve('dummyTid', 'dummyUid');
// Verify: The topic should be considered resolved
assert.strictEqual(topicResolved, true, 'Topic should be marked as resolved');
});
});
describe('.post', () => {
it('should fail to create topic with invalid data', async () => {
try {
Expand Down
23 changes: 23 additions & 0 deletions test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ describe('Utility Methods', () => {
done();
});

describe('Utility Methods', () => {
it('should return false if browser is not android', () => {
// Backup the original navigator
const originalNavigator = Object.getOwnPropertyDescriptor(global, 'navigator');

// Override the navigator
Object.defineProperty(global, 'navigator', {
value: { userAgent: 'Mozilla/5.0' },
configurable: true
});

// Your test logic here
// assert.strictEqual(isAndroidBrowser(), false);

// Restore the original navigator
if (originalNavigator) {
Object.defineProperty(global, 'navigator', originalNavigator);
} else {
delete global.navigator;
}
});
});

it('should return true if browser is android', (done) => {
global.navigator = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Android /58.0.3029.96 Safari/537.36',
Expand Down
20 changes: 20 additions & 0 deletions themes/nodebb-theme-persona/templates/partials/post_bar.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,24 @@
<!-- IMPORT partials/thread_tools.tpl -->
</div>
<!-- IMPORT partials/topic/reply-button.tpl -->
<!-- Code written by ChatGPT -->
<!-- Add Resolve button only available to users with correct privileges to resolve the topic -->
<!-- IF privileges.can_resolve -->
<button
component="topic/resolve"
class="btn btn-sm btn-default"
type="button"
{{{if resolve}}}disabled{{{end}}}>
<!-- Button content changes based on resolution state -->
{{{if resolve}}}
<!-- Displayed if the topic is already resolved -->
<i class="fa fa-fw fa-check"></i>
<span class="visible-sm-inline visible-md-inline visible-lg-inline">Already Resolved</span>
{{{else}}}
<!-- Displayed if the topic is not resolved yet -->
<i class="fa"></i>
<span class="visible-sm-inline visible-md-inline visible-lg-inline">Mark as Resolved</span>
{{{end}}}
</button>
<!-- ENDIF privileges.can_resolve -->
</div>
7 changes: 7 additions & 0 deletions themes/nodebb-theme-persona/templates/topic.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
{{{each icons}}}{@value}{{{end}}}
</span>
<span component="topic/title">{title}</span>
<!-- Code written by ChatGPT -->
<!-- Add resolve status -->
<!-- IF resolve -->
<small style="text-align: right; color: green;">Resolved</small>
<!-- ELSE -->
<small style="text-align: right; color: red;">Unresolved</small>
<!-- ENDIF resolve -->
</span>
</h1>

Expand Down