Creating new categories using plugins in NodeBB
I have implemented a function to create new categories using NodeBB plugins. This article explains how to implement this. Purpose As a forum for developers that I am currently developing, I would like to be able to handle the root category as a community. For example, if you create a root category such as JavaScript and PHP, you will be able to access it with a URL such as https://example.com/javascript. Each root category (community) has three groups. Administrator Member Ban (blacklist) These groups are used to set category permissions. Implementation Overview The implemented functions are as follows. Displaying the modal for creating communities WebSocket communication from the client side Creating categories on the server side Setting permissions Adding subcategories Adding a template First, we'll add a template for displaying the modal for creating communities. This will be placed in the templates directory as a .tpl file. Create a community × Name Description Cancel Create This template uses the Bootstrap modal component. The modal is identified by id=‘community-create-modal’ and the form input values can be obtained by the names name and description. Client-side implementation On the client-side, we will add JavaScript and CSS (Less). These are specified in plugin.json and loaded. { "id": "nodebb-plugin-caiz", "name": "NodeBB Plugin for Caiz", "description": "NodeBB Plugin for Caiz", "version": "1.0.0", "library": "./library.js", "staticDirs": { "static": "./static" }, "scripts": [ "static/modal.js" ], "less": [ "static/style.less" ] } In plugin.json, the following settings are made: staticDirs: Specify the directory for static files (JavaScript, CSS, images, etc.) scripts: Specify the JavaScript files to be loaded on the client side less: Specify the Less files to be loaded on the client side The client-side JavaScript implements the display of the modal and WebSocket communication. 'use strict'; async function getAlert() { return new Promise((resolve, reject) => { require(['alerts'], resolve); }); } $(document).ready(function () { // Trigger of showing modal $(document).on('click', '#create-community-trigger', function (e) { e.preventDefault(); $('#community-create-modal').modal('show'); }); // Click event in the modal $('#submit-community-create').on('click', async () => { const form = $('#community-create-form'); const formData = form.serializeArray().reduce((obj, item) => { obj[item.name] = item.value; return obj; }, {}); // Client validation const { alert } = await getAlert(); if (!formData.name) { alert({ type: 'warning', message: '[[caiz:error.name_required]]', timeout: 3000, }); return; } // Disable to the button const submitBtn = $(this); submitBtn.prop('disabled', true).addClass('disabled'); // Send to server by WebSocket socket.emit('plugins.caiz.createCommunity', formData, function(err, response) { if (err) { alert({ type: 'error', message: err.message || '[[caiz:error.generic]]', timeout: 3000, }); } else { $('#community-create-modal').modal('hide'); alert({ type: 'success', message: '[[caiz:success.community_created]]', timeout: 3000, }); // Redirect to community you made ajaxify.go(`/${response.community.handle}`); } form[0].reset(); submitBtn.prop('disabled', false).removeClass('disabled'); }); }); }); In this code: getAlert() loads the NodeBB alert module Set up the trigger to display the modal Monitor the click event on the create button: Get the form data and convert it into an object Perform client-side validation Disable the button to prevent double submissions Send the data to the server via WebSocket If successful, display the alert and redirect If there is an error, display the error message After processing is complete, reset the form and enable the button Notes In NodeBB, server-side functions are called using WebSocket, not Ajax. WebSocket functions are specified in module.exports.sockets, and if a function such as module.exports.sockets.caiz.createCommunity is defined on the server side, it can be called on the client side in an RPC-like way, for example

I have implemented a function to create new categories using NodeBB plugins. This article explains how to implement this.
Purpose
As a forum for developers that I am currently developing, I would like to be able to handle the root category as a community. For example, if you create a root category such as JavaScript
and PHP
, you will be able to access it with a URL such as https://example.com/javascript
.
Each root category (community) has three groups.
- Administrator
- Member
- Ban (blacklist)
These groups are used to set category permissions.
Implementation Overview
The implemented functions are as follows.
- Displaying the modal for creating communities
- WebSocket communication from the client side
- Creating categories on the server side
- Setting permissions
- Adding subcategories
Adding a template
First, we'll add a template for displaying the modal for creating communities. This will be placed in the templates
directory as a .tpl
file.
class="modal fade" id="community-create-modal" tabindex="-1" role="dialog">
class="modal-dialog" role="document">
class="modal-content">
class="modal-header">
class="modal-title">Create a community
class="modal-body">
class="modal-footer">
This template uses the Bootstrap modal component. The modal is identified by id=‘community-create-modal’
and the form input values can be obtained by the names name
and description
.
Client-side implementation
On the client-side, we will add JavaScript and CSS (Less). These are specified in plugin.json
and loaded.
{
"id": "nodebb-plugin-caiz",
"name": "NodeBB Plugin for Caiz",
"description": "NodeBB Plugin for Caiz",
"version": "1.0.0",
"library": "./library.js",
"staticDirs": {
"static": "./static"
},
"scripts": [
"static/modal.js"
],
"less": [
"static/style.less"
]
}
In plugin.json
, the following settings are made:
-
staticDirs
: Specify the directory for static files (JavaScript, CSS, images, etc.) -
scripts
: Specify the JavaScript files to be loaded on the client side -
less
: Specify the Less files to be loaded on the client side
The client-side JavaScript implements the display of the modal and WebSocket communication.
'use strict';
async function getAlert() {
return new Promise((resolve, reject) => {
require(['alerts'], resolve);
});
}
$(document).ready(function () {
// Trigger of showing modal
$(document).on('click', '#create-community-trigger', function (e) {
e.preventDefault();
$('#community-create-modal').modal('show');
});
// Click event in the modal
$('#submit-community-create').on('click', async () => {
const form = $('#community-create-form');
const formData = form.serializeArray().reduce((obj, item) => {
obj[item.name] = item.value;
return obj;
}, {});
// Client validation
const { alert } = await getAlert();
if (!formData.name) {
alert({
type: 'warning',
message: '[[caiz:error.name_required]]',
timeout: 3000,
});
return;
}
// Disable to the button
const submitBtn = $(this);
submitBtn.prop('disabled', true).addClass('disabled');
// Send to server by WebSocket
socket.emit('plugins.caiz.createCommunity', formData, function(err, response) {
if (err) {
alert({
type: 'error',
message: err.message || '[[caiz:error.generic]]',
timeout: 3000,
});
} else {
$('#community-create-modal').modal('hide');
alert({
type: 'success',
message: '[[caiz:success.community_created]]',
timeout: 3000,
});
// Redirect to community you made
ajaxify.go(`/${response.community.handle}`);
}
form[0].reset();
submitBtn.prop('disabled', false).removeClass('disabled');
});
});
});
In this code:
-
getAlert()
loads the NodeBB alert module - Set up the trigger to display the modal
- Monitor the click event on the create button:
- Get the form data and convert it into an object
- Perform client-side validation
- Disable the button to prevent double submissions
- Send the data to the server via WebSocket
- If successful, display the alert and redirect
- If there is an error, display the error message
- After processing is complete, reset the form and enable the button
Notes
In NodeBB, server-side functions are called using WebSocket, not Ajax. WebSocket functions are specified in module.exports.sockets
, and if a function such as module.exports.sockets.caiz.createCommunity
is defined on the server side, it can be called on the client side in an RPC-like way, for example, socket.emit(‘plugins.caiz.createCommunity’, formData, ...)
.
You can specify the time to close the alert automatically using the timeout option.
Server-side implementation
On the server side, you will receive WebSocket events and create a new category.
First, register the WebSocket event in library.js
. For WebSocket, there is no need to configure anything in plugin.json.
'use strict';
const sockets = require.main.require('./src/socket.io/plugins');
const Community = require('./libs/community');
// Register event of WebSocket
sockets.caiz = {};
sockets.caiz.createCommunity = Community.Create;
module.exports = plugin;
Next, implement the community creation process in libs/community.js
.
'use strict';
const db = require.main.require('./src/database');
const Plugins = require.main.require('./src/plugins');
const winston = require.main.require('winston');
const Categories = require.main.require('./src/categories');
const Privileges = require.main.require('./src/privileges');
const Groups = require.main.require('./src/groups');
const Base = require('./base');
const websockets = require.main.require('./src/socket.io/plugins');
const initialCategories = require.main.require('./install/data/categories.json');
class Community extends Base {
static async Create(socket, data) {
const { name, description } = data;
winston.info(`[plugin/caiz] Creating community: ${name}`);
const { uid } = socket;
if (!uid) {
throw new Error('Not logged in');
}
if (!name || name.length < 3) {
throw new Error('Community name is too short');
}
try {
const community = await Community.createCommunity(uid, { name, description });
return {
message: 'Community created successfully!',
community: community,
};
} catch (err) {
winston.error(`[plugin/caiz] Error creating community: ${err.message}`);
throw err;
}
}
static async createCommunity(uid, { name, description }) {
const ownerPrivileges = await Privileges.categories.getGroupPrivilegeList();
const guestPrivileges = ['groups:find', 'groups:read', 'groups:topics:read'];
// Create new top level category
const categoryData = {
name,
description: description || '',
order: 100,
parentCid: 0,
customFields: {
isCommunity: true
},
icon: 'fa-users',
};
const newCategory = await Categories.create(categoryData);
const cid = newCategory.cid;
// Create an owner community group
const ownerGroupName = `community-${cid}-owners`;
const ownerGroupDesc = `Owners of Community: ${name}`;
await Community.createCommunityGroup(ownerGroupName, ownerGroupDesc, uid, 1, 1);
await Groups.join(ownerGroupName, uid);
// Create a member community group
const communityGroupName = `community-${cid}-members`;
const communityGroupDesc = `Members of Community: ${name}`;
await Community.createCommunityGroup(communityGroupName, communityGroupDesc, uid, 0, 0);
await Groups.leave(communityGroupName, uid);
// Create a burned community group
const communityBanGroupName = `community-${cid}-banned`;
const communityBanGroupDesc = `Banned members of Community: ${name}`;
await Community.createCommunityGroup(communityBanGroupName, communityBanGroupDesc, uid, 1, 1);
await Groups.leave(communityBanGroupName, uid);
// Save owner group name in category data
await db.setObjectField(`category:${cid}`, 'ownerGroup', ownerGroupName);
// Setting privileges
await Privileges.categories.give(ownerPrivileges, cid, ownerGroupName);
const communityPrivileges = ownerPrivileges.filter(p => p !== 'groups:posts:view_deleted' && p !== 'groups:purge' && p !== 'groups:moderate');
await Privileges.categories.give(communityPrivileges, cid, communityGroupName);
await Privileges.categories.give([], cid, communityBanGroupName);
await Privileges.categories.rescind(ownerPrivileges, cid, 'guests');
await Privileges.categories.give(guestPrivileges, cid, 'guests');
await Privileges.categories.rescind(ownerPrivileges, cid, 'registered-users');
await Privileges.categories.give(guestPrivileges, cid, 'registered-users');
await Privileges.categories.give([], cid, 'banned-users');
// Create sub categories
await Promise.all(initialCategories.map((category) => {
return Categories.create({...category, parentCid: cid, cloneFromCid: cid});
}));
winston.info(`[plugin/caiz] Community created: ${name} (CID: ${cid}), Owner: ${uid}, Owner Group: ${ownerGroupName}`);
return newCategory;
}
}
module.exports = Community;
createCommunityGroup
is defined in libs/community.js
. The trick is to pass the flag as 0/1, not as a boolean.
static async createCommunityGroup(name, description, ownerUid, privateFlag = 0, hidden = 0) {
const group = await Groups.getGroupData(name);
if (group) return group;
return Groups.create({
name,
description,
private: privateFlag,
hidden,
ownerUid
});
}
The server-side implementation is as follows.
- In
library.js
: - Load the necessary modules
Register WebSocket events (
sockets.caiz.createCommunity = Community.Create
)In
libs/community.js
:Inherit the
Community
class from theBase
classHandle WebSocket events in the
Create
methodCreate the community in the
createCommunity
methodSet up groups and permissions
Creating subcategories
Error handling:
Login confirmation
Name validation
Error log output
Tips
When creating a new category, you can copy the category's initial settings (such as permissions) by passing cloneFromCid
.
return Categories.create({...category, parentCid: cid, cloneFromCid: cid});
As for permission settings, registered users etc. will have the default permission settings applied. Therefore, it is possible to set permissions flexibly by first revoking all permissions and then granting the necessary permissions.
// Revoke all permissions
// 全権限を剥奪
await Privileges.categories.rescind(ownerPrivileges, cid, 'registered-users');
// Grant the necessary permissions
await Privileges.categories.give(guestPrivileges, cid, 'registered-users');
For WebSocket communication, if there is an error, you can handle it with exception handling. If the process is successful, you can return a variable with return, and the client side will receive it
Summary
Using the NodeBB plugin, we have implemented a function to create new categories. The main points are as follows
- Display a modal using a template
- Implement WebSocket communication on the client side
- Create categories and subcategories on the server side
- Set permissions and clone
With this implementation, users can easily create new communities. In addition, since subcategories are also created automatically, it is now possible to efficiently perform initial settings for communities.