NodeBB v4.0.0 Beta

Last friday I quietly tagged a commit on the activitypub branch with v4.0.0-beta.1, which signals that the ActivityPub integration is now ready for beta testing.

For the most complete (yet readable) list of new functionality from the alpha, check out the "Road to Beta" project page.

tl;dr — some new features and a lot of fixes

  • Editing a category now issues an Update(Actor)
  • replies is now populated and responds with an OrderedCollection of direct replies (see the post about that)
  • Proper sharedInbox support
  • Better indicators for content that comes from non-local (aka "remote") users
  • Conversational context synchronization mechanic (corresponding post for that)
  • Moving a topic out of the "uncategorized" category will now federate out an Announce

By and large most incompatibilties have been resolved, although if you do find some issues, please do let me know in the corresponding bug report thread.

I'm looking to wrap up the year with some of the more difficult projects I've put off

  • Object Integrity Proofs, which will also enable Inbox Forwarding
  • Better handling of "Open in App" signals from third-party instances
  • Post visibility support (or at least better handling so non-public messages aren'y unceremoniously dropped!)
  • Ongoing integration with FEPs 400e and 7888
13 Likes

Great, the sooner this goes to main branch the better. Admins wont have to activate it but they are more likely to try it at least when its there to switch on.
I jokingly said to @phenomlab (it was 50% joke and 50% serious), my benchmark of when this gets very widespread acceptance is when he gets onboard 😊

I hope @phenomlab doesn't mind me complementing that his sudonix implementation is one of the best implementations of nodebb, and his thorough approach would be an asset to the progression.

It's always very exciting when we encounter developers who take the time to learn how a product works, and expands on it to build something really neat.

@phenomlab did just that with his themes and various plugins, and hopefully we're able to provide a good base for him to continue his experimentation.

The other day @baris discovered that QT had updated their forum to base their theme off of Harmony which is super cool.

I don't know if they participate on these forums, but if they do, I don't know who they are 😅

@julian The link to FEP-400e actually goes to FEP-7888. Anyway, great job!

@julian The link to FEP-400e actually goes to FEP-7888. Anyway, great job!

@hongminhee@fosstodon.org and you as well, on the upcoming launch of v1!

1 Like

Sounds great. Thank you for your work.

Not sure if you have plans on this, but implementation of FEP 61cf (Federated Single Signon) would be a game changer for forums.

With ActivityPub, people can follow remotely and get notifications. With OpenWebAuth, they can log in and participate with their existing Social Identity. It also lets them interact with posts that didn’t show up in their inbox.

@scott@authorship.studio at this time there are no plans for FEP 61cf but that doesn't not mean it will never happen. Simply that there are too few hours in the day 😁

1 Like

I totally understand the issue with the number of hours in the day. I have a lot of projects going too.

I am working on creating an ecosystem of websites that all use Magic Signon (OpenWebAuth). Social media websites, forum communities, productivity apps, online courses, etc. You can use the same social identity to log into all of them.

The more projects that support it, the more people will use it.

A forum project like yours would make a great addition. :slight_smile:

at the risk of igniting a protocol war, i’m not sure OpenWebAuth is the best solution for cross-domain identity, for a few reasons:

  • it has reliances on older and not-widely-implemented technologies. the rest of the tech space is converging on oauth and related technologies, and not the old http signatures cavage draft stuff. using webfinger to look up a “magic endpoint” for the user, and again to look up the “root url” to find a bespoke “token endpoint”, is simply duplicating a lot of work that has been standardized in other specifications.
  • it makes no proper distinction between the key and the actor. the problem with current public key infrastructure in fedi is that keys are not only custodial, but they may be shared across the entire server. so anyone else on your server may be able to access remote resources “on your behalf”, as it were, in the case that a single keypair existed and was declared for every single actor on the local origin.
  • it relies on query parameters which can easily leak into logs or get copied-and-pasted accidentally by a user. the proper place for authorization tokens and such is in the Authorization header (which does not generally get logged).
  • it assumes that there is a “home instance” and does not support the possibility of external identity providers that are not themselves fediverse instances.
1 Like

@trwnh@socialhub.activitypub.rocks @scott@authorship.studio If not for HTTP signatures, would we be looking toward proofs to guarantee authenticity of received activities?

depends what you’re trying to assert or prove. signing http messages in general is something that could still be done, if your goal is to assert who made the http request. embedding a signature on a document is fine, if your goal is to assert who authored the document and that the document has not been modified. (or you could limit it to just the content, or some arbitrary metadata, or whatever.)

for any key-based verification methods, you need to be wary of key reuse. it’s possible for the keyId to be different, but for the key material to be the same as some other key. you can generally tell that an actor is represented by a key, and then the key can claim to be controlled by that actor, but you don’t know if the associated private key is in fact being controlled by other actors unknown to you, or if other actors are represented by the same public key material. so some level of deduplication of public keys may be needed before you can authenticate an identity. of course the “real” problem is that the keys are most likely custodial, and rather than try to discover and enumerate every single actor from a given domain, most implementers just assume that any keyId on a given domain is in reality controlled by the software running on that domain. (the same-origin policy, basically.)

so in conclusion, if you’re trying to know that a given identity is behind the request, then the best you can tell right now is that their server is making the request. whereas with stuff like fedcm oidc or fedcm indieauth, the request is happening in the browser.

1 Like

I get the following error trying to upgrade from v3.9.10 on my dev install:

4. Updating NodeBB data store schema...
Parsing upgrade scripts...
OK | 8 script(s) found, 145 skipped
  → [2024/2/22] Setting up default configs/privileges re: ActivityPub...Error occurred
Error occurred during upgrade: Error: [[error:invalid-data]]
    at Meta.slugTaken (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/meta/index.js:34:9)
    at Object.wrapperCallback [as slugTaken] (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js:46:11)
    at generateHandle (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/categories/create.js:153:26)
    at Object.wrapperCallback [as generateHandle] (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js:46:11)
    at /var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js:25:36
    at Array.map (<anonymous>)
    at Object.method (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js:23:42)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async Upgrade.process (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js:161:4)
    at async Upgrade.run (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js:110:2)

@sweetp can you please add console.log(names); to line 22 (the blank line) in src/upgrade/4.0.0/activitypub_setup.js, and run ./nodebb upgrade activitypub_setup again?

@Julian here you go:

Parsing upgrade scripts...
OK | 1 script(s) found
  → [2024/2/22] Setting up default configs/privileges re: ActivityPub...[
  { name: 'Cookie 7' },
  { name: 'TuneTag' },
  { name: 'Cookie 5' },
  { name: 'News' },
  { name: 'Minim' },
  { name: 'Cookie 4' },
  { name: 'ColorWell' },
  { name: 'Cookie 6' },
  { name: 'WiFiSpoof' },
  { name: 'SessionRestore JS' },
  { name: 'Privatus' },
  { name: 'USBclean' },
  { name: 'SessionRestore' },
  { name: 'Hides' },
  { name: 'Xliff Editor' },
  { name: 'Other apps' },
  { name: 'Invisible' },
  { name: 'Archived' },
  { name: 'Misc' },
  { name: null }
]
Error occurred
2024-10-03T18:18:47.515Z [4568/1713751] - error: uncaughtException: [[error:invalid-data]]
Error: [[error:invalid-data]]
    at Meta.slugTaken (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/meta/index.js:34:9)
    at Object.wrapperCallback [as slugTaken] (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js:46:11)
    at generateHandle (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/categories/create.js:153:26)
    at Object.wrapperCallback [as generateHandle] (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js:46:11)
    at /var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js:25:36
    at Array.map (<anonymous>)
    at Object.method (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js:23:42)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async Upgrade.process (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js:161:4)
    at async Upgrade.runParticular (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js:118:2) {"date":"Thu Oct 03 2024 20:18:47 GMT+0200 (Central European Summer Time)","error":{},"exception":true,"os":{"loadavg":[0.21,0.17,0.06],"uptime":2804105.47},"process":{"argv":["/opt/plesk/node/22/bin/node","/var/www/vhosts/domain.com/test.domain.com/NodeBB/nodebb","upgrade","activitypub_setup"],"cwd":"/var/www/vhosts/domain.com/test.domain.com/NodeBB","execPath":"/opt/plesk/node/22/bin/node","gid":1003,"memoryUsage":{"arrayBuffers":869789,"external":4802391,"heapTotal":92000256,"heapUsed":63868520,"rss":177631232},"pid":1713751,"uid":10011,"version":"v22.9.0"},"stack":"Error: [[error:invalid-data]]\n    at Meta.slugTaken (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/meta/index.js:34:9)\n    at Object.wrapperCallback [as slugTaken] (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js:46:11)\n    at generateHandle (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/categories/create.js:153:26)\n    at Object.wrapperCallback [as generateHandle] (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js:46:11)\n    at /var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js:25:36\n    at Array.map (<anonymous>)\n    at Object.method (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js:23:42)\n    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at async Upgrade.process (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js:161:4)\n    at async Upgrade.runParticular (/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js:118:2)","trace":[{"column":9,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/meta/index.js","function":"Meta.slugTaken","line":34,"method":"slugTaken","native":false},{"column":11,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js","function":"Object.wrapperCallback [as slugTaken]","line":46,"method":"wrapperCallback [as slugTaken]","native":false},{"column":26,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/categories/create.js","function":"generateHandle","line":153,"method":null,"native":false},{"column":11,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/promisify.js","function":"Object.wrapperCallback [as generateHandle]","line":46,"method":"wrapperCallback [as generateHandle]","native":false},{"column":36,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js","function":null,"line":25,"method":null,"native":false},{"column":null,"file":null,"function":"Array.map","line":null,"method":"map","native":false},{"column":42,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrades/4.0.0/activitypub_setup.js","function":"Object.method","line":23,"method":"method","native":false},{"column":5,"file":"node:internal/process/task_queues","function":"process.processTicksAndRejections","line":105,"method":"processTicksAndRejections","native":false},{"column":4,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js","function":"async Upgrade.process","line":161,"method":"process","native":false},{"column":2,"file":"/var/www/vhosts/domain.com/test.domain.com/NodeBB/src/upgrade.js","function":"async Upgrade.runParticular","line":118,"method":"runParticular","native":false}]}
$

@sweetp what is the last category, and how come it has no name?

@Julian Im not sure, is there any way to purge it?

@sweetp does it correspond to an existing category? Or are all of your categories represented in the output above?

@Julian all my categories are repesented by the log output

@Julian I filtered out the null names and the upgrade went through:

"use strict";

const db = require(“…/…/database”);
const meta = require(“…/…/meta”);
const categories = require(“…/…/categories”);
const slugify = require(“…/…/slugify”);

module.exports = {
name: “Setting up default configs/privileges re: ActivityPub”,
timestamp: Date.UTC(2024, 1, 22),
method: async () => {
// Disable ActivityPub (upgraded installs have to opt-in to AP)
meta.configs.set(“activitypubEnabled”, 0);

// Set default privileges for world category
const install = require("../../install");
await install.giveWorldPrivileges();

// Run through all categories and ensure their slugs are unique (incl. users/groups too)
const cids = await db.getSortedSetMembers("categories:cid");
const names = await db.getObjectsFields(
  cids.map((cid) =&gt; `category:${cid}`),
  cids.map(() =&gt; "name"),
);

const nullIndexes = names
  .map((element, index) =&gt; (element["name"] === null ? index : -1)) // mark null elements
  .filter((index) =&gt; index !== -1);

let filteredNames = names.filter(element =&gt; element["name"] !== null);
let filteredCids = cids.filter((_, index) =&gt; !nullIndexes.includes(index));

const handles = await Promise.all(
  filteredCids.map(async (cid, idx) =&gt; {
    const { name } = filteredNames[idx];
    const handle = await categories.generateHandle(slugify(name));
    return handle;
  }),
);

await Promise.all([
  db.setObjectBulk(
    filteredCids.map((cid, idx) =&gt; [`category:${cid}`, { handle: handles[idx] }]),
  ),
  db.sortedSetAdd("categoryhandle:cid", filteredCids, handles),
]);

},
};