Skip to content

NodeBB Development

Stay tuned here to hear more about new releases and features of NodeBB!

3.5k Topics 22.2k Posts

Subcategories


  • Posts from the NodeBB Development Blog
    96 Topics
    778 Posts
    julianJ

    Occasionally, we will get asked whether there are any differences between our hosted service and the open source project.

    It is as though we are holding back some great features and only allowing our paying customers access them! Conversely, it could be assumed that because we are hosting the software for others, that we would somehow out of self-interest or for economic reasons, deliver an inferior version with limitations.

    I'd like to say upfront that this is not the case for NodeBB.

    When you use our hosted service, you receive the same great NodeBB software that you can get for free off of our GitHub repository.

    What we're selling is support, maintenance, upgrades, and peace of mind delivered by our world-class† support team.

    You definitely can host NodeBB on your own! We've strived for years to deliver a piece of software that runs lean and fast on minimal hardware, great docs (some contributed by other admins!) that help you get up to speed quickly, and a fantastic community that will help you if you get stuck.

    The reason I take this principled stand is simple — I think it's unfair when artificial limitations are placed on software just for the purpose of getting customers to pay more.

    We've seen all this time and time again:

    You can't install any plugin you want, just a select few from a small list You can only have X units (tickets, posts, etc) of whatever you're using You can only have X admins/owners You can't see any messages older than X days

    These limitations are all artificial, and serve to restrict the use of something to the bare minimum. Anything extra is — of course — available for the right price.

    We don't do that. We tell everybody that NodeBB is powerful enough to run huge communities, and we stand by it. We tell everybody that NodeBB is flexible enough to look and function however you want, and we stand by it.

    These are the real limitations we impose on our hosting service:

    Hard drive space for uploads are imposed by our upstream provider and are set, though we are happy to add additional drive volumes for a fee) We have soft "pageview" limits that any user on our hosting can exceed (in fact, many do). We set them purely as a benchmark for the point at which your NodeBB may slow down depending on the type of load that you get, and encourage dialogue to make sure that you're on the right plan (server resources, etc.) We do not allow shell access for security reasons (and if you needed it, you probably could self-host)

    So please do rest assured when I and others tell you that what you see is what you get. No more, no less. I'd rather everybody get to use the best of NodeBB, instead of serving a special feature-reduced version for others.

    † I'm going to go out on limb here and say that we're probably the most qualified people to maintain NodeBB. Feel free to disagree 😉

  • You have a cool idea about NodeBB? Post it here.

    805 Topics
    5k Posts
    赵帅更

    Is anyone using WeChat to scan and log in? The plugin seems to be malfunctioning. Does anyone have a solution? Thx.
    Plugin: https://github.com/NodeBB-China/nodebb-plugin-sso-wechat-web

  • Found a bug? Why not make a bug report here?
    1k Topics
    7k Posts
    barisB

    Created an issue https://github.com/NodeBB/NodeBB/issues/12474

  • Focused discussion related to ActivityPub integration in NodeBB

    43 Topics
    492 Posts
    julianJ

    @[email protected] @[email protected] indeed, four different implementations is not ideal. We're at the point where they're all vying for attention.

    Would need to weigh pros and cons of each and see whether there is one winner we can all agree on.

  • Help Translate NodeBB
  • Composer design ideas

    3
    3 Votes
    3 Posts
    1k Views
    E

    @baris said in Composer design ideas:

    Looks good, another improvement would be to make the composer a regular element in the body instead of static positioned div overlayed on top of everything else on mobile.

    that would probably help too

  • Modify arguments of user:profileLinks hook

    5
    1 Votes
    5 Posts
    2k Views
    P

    @jarey I believe the approach is to add breaking changes to major releases so, probably for 2.0.0 I think

  • Topic watching change

    1
    2 Votes
    1 Posts
    1k Views
    barisB

    Please see https://github.com/NodeBB/NodeBB/pull/4662

    If you have a custom theme that has a partials/topic/watch.tpl you will need to update it for 1.1.0.

    You can see the changes you need to make in https://github.com/NodeBB/nodebb-theme-persona/pull/278/files

    Persona, vanilla and lavender themes are already updated.

  • 0 Votes
    4 Posts
    2k Views
    jareyJ

    Anyone knows if theres the possibility of registering an user link in the dropdown, that will be public only? Like the case for example of the "chat link", but registered vía filter:user.profileLinks hook.

    Since in that hook theres no user info, i cannot check if the user visualizing the page is the users itself that is visualizing his/her profile or if the user its logged in, to restrict the link registration.

    I want to implement the functionality of "ignore users" showing a link that can be used to ignore, unignore from the user profile. But it has no sense to show the link "ignore/unignore user" if it is the user itself or the user visiting the profile is not logged in.

    I know it can be easilly achived by adding one more attribute to the link object and modifying the menu.tpl with minor changes to reflect this behaviour (checking the new field), and without altering the existing behaviour of the theme. If the answer to the previous question is "no", what is needed to get a pull request including this behaviour accepted? Modification in all stable themes? (persona,lavender,vanilla?).
    Or would it be more suitable a request of a new action hook for action:user:viewProfile for example? To be able to make business logic checks with the data, and altering the content sent to the page for example?

    PS: Is there any way of contributing to document the list of hooks with a short explanation of each one? For example when they're fired and wich information do they send? I think it would be a good improvement for the documentation in order to make it easy to the plugin dev to without the need to dive into the core and check each hook one by one looking for one satisfying their needs. I wouldn't mind to throw 2/3 lines explaining the hooks I found useful that i'm seeing on my little plugin experiments from a noob perspective. Ping @administrators

    Thanks again and sorry for modifying the thread question.

  • 0 Votes
    3 Posts
    1k Views
    jareyJ

    Thanks for your reply @julian

  • 1 Votes
    6 Posts
    3k Views
    N

    @baris you have any solution for the Cat A.1.1 ?

  • How to make Category Sections?

    11
    0 Votes
    11 Posts
    6k Views
    yariplusY

    @julian I checked a couple different forum softwares, and all of them were purely cosmetic.

    A few of them had privileges settings that propagated to their children, but this was completely unnecessary and just a shortcut for assigning to each child.

  • 0 Votes
    1 Posts
    825 Views
    SuperMikeS

    I am writing a plugin that depends on another one.

    So I have to make sure the depended plugin finish initializing(static:app.load), then I can start my init work.

    Is there a way to do this ? thanks 😄

  • 1 Votes
    6 Posts
    2k Views
    J

    Well that works! Thanks!

  • 0 Votes
    2 Posts
    1k Views
    anandA

    I think SSO will do the magic. But you will have to make your own plug-in to Integrate.

  • 0 Votes
    3 Posts
    1k Views
    BriB

    Admin > settings > General

  • filter:<template>.build changes

    1
    5 Votes
    1 Posts
    2k Views
    barisB

    Related issue https://github.com/NodeBB/NodeBB/issues/4587

    As discussed in the issue, we had a bunch of hooks to modify a page on some routes like filter:recent.build. These were added as required but most pages didn't have a hook so if you needed one you would have to ask us to add it and wait for the next release. With the above change every route has a hook. The hook name is derived from the template name passed to the res.render(template, data); method. For example if your template is named myAwesomePage, you render it with res.render('myAwesomePage', data); and the hook that will be fired is filter:myAwesomePage.build

    Every hook gets 3 parameters {req: req, res: res, templateData: data}, data is the parameter you pass into the res.render method.

    We had a few inconsistencies in the parameters passed to the hook and the hook name below are the changes that needs to be fixed if you have a plugin that is listening to these hooks.

    filter:groups.build is now called filter:group/details.build as that is the correct name for that template. filter:popular.build used to pass in a extra term parameter, this moved into the templateData parameter. filter:header.build this is removed it was deprecated a while ago. Use filter:navigation.available instead. filter:search.build parameters changed, data is no longer passed in use req.query and req.params instead. results is now the templateData parameter.

    Feel free to ask any questions if anything isn't clear.

  • categories loop on a plugin

    6
    0 Votes
    6 Posts
    2k Views
    yariplusY

    Sure.

    library.js

    Plugin = module.exports; var categories = require.main.require('./src/categories'); // init hook Plugin.init = function (data, callback) { var router = data.router; var middleware = data.middleware; // Create route to render the template to. router.get('/example-categories', middleware.buildHeader, renderExampleCategories); router.get('/api/example-categories', renderExampleCategories); function renderExampleCategories(req, res, next) { // Get all the visible categories. categories.getCategoriesByPrivilege('cid:0:children', req.uid, 'find', function(err, categoryData) { if (err) return next(err); // Put the categories in a tree format. categories.flattenCategories([], categoryData); // Send the data to the template. `example-categories.tpl` res.render('example-categories', {categories: categoryData}); }); } };

    example-categories.tpl

    <!-- BEGIN categories --> <a href="{config.relative_path}/category/{categories.slug}" itemprop="url">{categories.name}</a> <br> <!-- BEGIN categories.children --> - <a href="{config.relative_path}/category/{categories.children.slug}" itemprop="url">{categories.children.name}</a> <br> <!-- END categories.children --> <!-- END categories -->

    Should give you a list like this

  • 1 Votes
    18 Posts
    6k Views
    jareyJ

    @yariplus said in Noob Plugin question: Alterin error message on filter hook.:

    Yep, you can use this function here the same way.

    Link Preview Image nodebb-plugin-tagstitle/library.js at master · jarey/nodebb-plugin-tagstitle

    Contribute to jarey/nodebb-plugin-tagstitle development by creating an account on GitHub.

    favicon

    GitHub (github.com)

    Just declare it at the global scope so that your hook can see it.

    Thank you very much @yariplus ; I achieved what i needed thanks to your help.

    I was able to see my custom page with my custom messages like you pointed out.

    Then, because i only wanted to change the message displayed on the 403 error regarding the logic on my plugin i was able to change it to using another method; only if it could help anyone else, i just imported the helpers module and called the function notAllowed with my custom message:

    var helpers = require.main.require('./src/controllers/helpers'); // filter:topic.build tagsTitle.topicBuild = function (data, callback) { if (data.templateData.privileges.errorMessage) { helpers.notAllowed(data.req,data.res, data.templateData.privileges.errorMessage); }else{ callback(null, data); } };

    Thanks again. I learned a lot of basic stuff just trying your suggestions.

    Kind regards.

  • In page Composer, is it possible?

    31
    3 Votes
    31 Posts
    14k Views
    BriB

    Composer is uncomposed....

    When clicking into the text box I'm seeing this on safari iOS:
    0_1461357163592_image.png

    Comes back into view when I start typing

  • same problem bCrypt

    3
    0 Votes
    3 Posts
    2k Views
    D

    version 0.0.3,
    Well this problem was solved, in the function i was written 3 params and are 4 and the last is the callback . I have other problems now , i get the hash but when the value set to pasword not work, chage inside the function but out is the same value. Thanks
    bcrypt.hash(user.password, usersalt, null, function (err, hash) {
    if (err) {
    return next(err);
    }

    user.password = hash });
  • 0 Votes
    5 Posts
    2k Views
    M

    i hope the nodebb dev team could consider our requests.We will make NodeBB better!
    @administrators

  • Japanese Localization

    3
    0 Votes
    3 Posts
    1k Views
    snodejokeS

    Thanks Julian, you're a legend!

  • Embedded template buttons don't work

    8
    0 Votes
    8 Posts
    2k Views
    A

    Ok, so for example, if I wanted to get the js for the reply, favorite and upvote button functionatliy, where/how would I grab that script? I noticed the buttons all use the component system, so do I have to somehow include components.js in the new template? How would I go about doing that?

  • Oauth not using refresh token

    1
    0 Votes
    1 Posts
    714 Views
    ?

    Hi guys,
    I have been using this plugin (like everyone else) [https://github.com/julianlam/nodebb-plugin-sso-oauth] to make a SSO plugin for Nodebb.

    This particular variant will be for Magento 1 where they use OAuth 1.0. Generally speaking, the plugin works well with a few code tweaks. However, it always seems to request a new token rather then using the existing, or refreshing if needed. I can guarantee the issue is my code .. but I seem to be stuck on where to begin on figuring out what should be done here.

    I have looked around, and in the other SSO plugins that are around, but it seems like it automagically happens without much code done to make it so. Everything else works but that last little step..

    [code]
    (function(module) {
    "use strict";

    var User = module.parent.require('./user'), Groups = module.parent.require('./groups'), meta = module.parent.require('./meta'), db = module.parent.require('../src/database'), passport = module.parent.require('passport'), fs = module.parent.require('fs'), path = module.parent.require('path'), nconf = module.parent.require('nconf'), winston = module.parent.require('winston'), async = module.parent.require('async'); var authenticationController = module.parent.require('./controllers/authentication'); var constants = Object.freeze({ type: 'oauth', name: 'Magento', admin: { 'route': '/plugins/sso-magento', 'icon': 'fa-twitter-square' }, }); var Magento = {}, passportOAuth, opts; Magento.init = function(data, callback) { function render(req, res, next) { res.render('admin/plugins/sso-magento', {}); } data.router.get('/admin/plugins/sso-magento', data.middleware.admin.buildHeader, render); data.router.get('/api/admin/plugins/sso-magento', render); callback(); }; Magento.getStrategy = function(strategies, callback) { meta.settings.get('sso-magento', function(err, settings) { if (!err && settings['key'] && settings['secret'] && settings['requesttoken'] && settings['accesstoken'] && settings['userauthorize'] && settings['userroute']) { passportOAuth = require('passport-oauth')[constants.type === 'oauth' ? 'OAuthStrategy' : 'OAuth2Strategy']; var oauth = { requestTokenURL: settings['requesttoken'], accessTokenURL: settings['accesstoken'], userAuthorizationURL: settings['userauthorize'], consumerKey: settings['key'], consumerSecret: settings['secret'], callbackURL: settings['callback'] }; if (constants.type === 'oauth') { // OAuth options opts = oauth; passportOAuth.Strategy.prototype.userProfile = function(token, secret, params, done) { this._oauth.get(settings['userroute'], token, secret, function(err, body, res) { if (err) { return done(err); } try { var json = JSON.parse(body); Magento.parseUserReturn(json, function(err, profile) { if (err) return done(err); profile.provider = constants.name; done(null, profile); }); } catch(e) { done(e); } }); }; } passport.use(constants.name, new passportOAuth(opts, function(token, secret, profile, done) { Magento.login({ oAuthid: profile.id, handle: profile.displayName, email: profile.emails[0].value, isAdmin: profile.isAdmin }, function(err, user) { if (err) { return done(err); } done(null, user); }); })); strategies.push({ name: constants.name, url: '/auth/' + constants.name, callbackURL: '/auth/' + constants.name + '/callback', icon: 'fa-check-square', scope: (constants.scope || '').split(',') }); callback(null, strategies); } else { callback(new Error('Magento OAuth Configuration is invalid')); } }); } Magento.parseUserReturn = function(data, callback) { for (var key in data) break; var profile = {}; profile.id = key; profile.displayName = data[key].firstname + data[key].lastname; profile.emails = [{ value: data[key].email }]; callback(null, profile); } Magento.addMenuItem = function(custom_header, callback) { custom_header.authentication.push({ "route": constants.admin.route, "icon": constants.admin.icon, "name": constants.name }); callback(null, custom_header); }; Magento.login = function(payload, callback) { Magento.getUidByOAuthid(payload.oAuthid, function(err, uid) { if(err) { return callback(err); } if (uid !== null) { // Existing User callback(null, { uid: uid }); } else { // New User var success = function(uid) { // Save provider-specific information to the user User.setUserField(uid, constants.name + 'Id', payload.oAuthid); db.setObjectField(constants.name + 'Id:uid', payload.oAuthid, uid); if (payload.isAdmin) { Groups.join('administrators', uid, function(err) { callback(null, { uid: uid }); }); } else { callback(null, { uid: uid }); } }; User.getUidByEmail(payload.email, function(err, uid) { if(err) { return callback(err); } if (!uid) { User.create({ username: payload.handle, email: payload.email }, function(err, uid) { if(err) { return callback(err); } success(uid); }); } else { success(uid); // Existing account -- merge } }); } }); }; Magento.getUidByOAuthid = function(oAuthid, callback) { db.getObjectField(constants.name + 'Id:uid', oAuthid, function(err, uid) { if (err) { return callback(err); } callback(null, uid); }); }; Magento.deleteUserData = function(uid, callback) { async.waterfall([ async.apply(User.getUserField, uid, constants.name + 'Id'), function(oAuthIdToDelete, next) { db.deleteObjectField(constants.name + 'Id:uid', oAuthIdToDelete, next); } ], function(err) { if (err) { winston.error('[sso-oauth] Could not remove OAuthId data for uid ' + uid + '. Error: ' + err); return callback(err); } callback(null, uid); }); }; module.exports = Magento;

    }(module));
    [/code]