From 1b64fdb5b329181b67c095809fcb1de3d771d803 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 5 Mar 2024 09:56:15 -0500 Subject: [PATCH] feat: allow user.search to handle remote handles, beginning of mentions support --- public/src/modules/slugify.js | 4 ++-- src/activitypub/actors.js | 25 +++++++++++++++++++++++++ src/activitypub/helpers.js | 12 +++++++++--- src/activitypub/mocks.js | 15 +++++++++++++++ src/user/index.js | 2 ++ src/user/search.js | 25 ++++++++++++++++++++++--- 6 files changed, 75 insertions(+), 8 deletions(-) diff --git a/public/src/modules/slugify.js b/public/src/modules/slugify.js index 3046ed2b94..159b356e47 100644 --- a/public/src/modules/slugify.js +++ b/public/src/modules/slugify.js @@ -10,8 +10,8 @@ window.slugify = factory(XRegExp); } }(function (XRegExp) { - const invalidUnicodeChars = XRegExp('[^\\p{L}\\s\\d\\-_]', 'g'); - const invalidLatinChars = /[^\w\s\d\-_]/g; + const invalidUnicodeChars = XRegExp('[^\\p{L}\\s\\d\\-_@.]', 'g'); + const invalidLatinChars = /[^\w\s\d\-_@.]/g; const trimRegex = /^\s+|\s+$/g; const collapseWhitespace = /\s+/g; const collapseDash = /-+/g; diff --git a/src/activitypub/actors.js b/src/activitypub/actors.js index 7143d7cfe3..9281a28608 100644 --- a/src/activitypub/actors.js +++ b/src/activitypub/actors.js @@ -3,6 +3,7 @@ const winston = require('winston'); const db = require('../database'); +const user = require('../user'); const utils = require('../utils'); const activitypub = module.parent.exports; @@ -89,9 +90,33 @@ Actors.assert = async (ids, options = {}) => { bulkSet.push(['followersUrl:uid', Object.fromEntries(followersUrlMap)]); } + const exists = await db.isSortedSetMembers('usersRemote:lastCrawled', profiles.map(p => p.uid)); + const uidsForCurrent = profiles.map((p, idx) => (exists[idx] ? p.uid : 0)); + const current = await user.getUsersFields(uidsForCurrent, ['username', 'fullname']); + const searchQueries = profiles.reduce((memo, profile, idx) => { + if (profile) { + const { username, fullname } = current[idx]; + + if (username !== profile.username) { + memo.remove.push(['ap.preferredUsername:sorted', `${username.toLowerCase()}:${profile.uid}`]); + memo.add.push(['ap.preferredUsername:sorted', 0, `${profile.username.toLowerCase()}:${profile.uid}`]); + } + + if (fullname !== profile.fullname) { + memo.remove.push(['ap.name:sorted', `${fullname.toLowerCase()}:${profile.uid}`]); + memo.add.push(['ap.name:sorted', 0, `${profile.fullname.toLowerCase()}:${profile.uid}`]); + } + } + + return memo; + }, { remove: [], add: [] }); + await Promise.all([ db.setObjectBulk(bulkSet), db.sortedSetAdd('usersRemote:lastCrawled', ids.map((id, idx) => (profiles[idx] ? now : null)).filter(Boolean), ids.filter((id, idx) => profiles[idx])), + db.sortedSetRemove('ap.preferredUsername:sorted', searchQueries.remove), + db.sortedSetRemoveBulk(searchQueries.remove), + db.sortedSetAddBulk(searchQueries.add), ]); return actors.every(Boolean); diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 7153deb6c9..2bb6f14ab6 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -57,10 +57,16 @@ Helpers.query = async (id) => { ({ href: actorUri } = actorUri); } - const { publicKey } = body; + const { subject, publicKey } = body; + const payload = { subject, username, hostname, actorUri, publicKey }; - webfingerCache.set(id, { username, hostname, actorUri, publicKey }); - return { username, hostname, actorUri, publicKey }; + const claimedId = subject.slice(5); + webfingerCache.set(claimedId, payload); + if (claimedId !== id) { + webfingerCache.set(id, payload); + } + + return payload; }; Helpers.generateKeys = async (type, id) => { diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 2cb2769cbb..5f90fce425 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -9,6 +9,7 @@ const user = require('../user'); const categories = require('../categories'); const posts = require('../posts'); const topics = require('../topics'); +const plugins = require('../plugins'); const utils = require('../utils'); const activitypub = module.parent.exports; @@ -227,6 +228,20 @@ Mocks.note = async (post) => { })); } + const mentionsEnabled = await plugins.isActive('nodebb-plugin-mentions'); + if (mentionsEnabled) { + const mentions = require.main.require('nodebb-plugin-mentions'); + const matches = await mentions.getMatches(post.content); + + if (matches.size) { + tag = tag || []; + Array.from(matches).map(match => ({ + type: 'Mention', + name: match, + })); + } + } + const object = { '@context': 'https://www.w3.org/ns/activitystreams', id, diff --git a/src/user/index.js b/src/user/index.js index 997a5f5f8b..7a48c2d08b 100644 --- a/src/user/index.js +++ b/src/user/index.js @@ -111,6 +111,8 @@ User.getUidByUserslug = async function (userslug) { return 0; } + // fix this! Forces a remote call, this is bad. Should be done in actors.assert + // then mentions. should return actor uri or url or something to parsePost. if (userslug.includes('@')) { const { actorUri } = await activitypub.helpers.query(userslug); return actorUri; diff --git a/src/user/search.js b/src/user/search.js index 2713b3a8dd..c636ca149e 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -7,6 +7,7 @@ const meta = require('../meta'); const plugins = require('../plugins'); const db = require('../database'); const groups = require('../groups'); +const activitypub = require('../activitypub'); const utils = require('../utils'); module.exports = function (User) { @@ -42,9 +43,21 @@ module.exports = function (User) { } else { const searchMethod = data.findUids || findUids; uids = await searchMethod(query, searchBy, data.hardCap); + + const mapping = { + username: 'ap.preferredUsername', + fullname: 'ap.name', + }; + if (meta.config.activitypubEnabled && mapping.hasOwnProperty(searchBy)) { + uids = uids.concat(await searchMethod(query, mapping[searchBy], data.hardCap)); + } } uids = await filterAndSortUids(uids, data); + if (data.hardCap > 0) { + uids.length = data.hardCap; + } + const result = await plugins.hooks.fire('filter:users.search', { uids: uids, uid: uid }); uids = result.uids; @@ -62,7 +75,8 @@ module.exports = function (User) { const userData = await User.getUsers(uids, uid); searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); - searchResult.users = userData.filter(user => user && user.uid > 0); + searchResult.users = userData.filter(user => (user && + utils.isNumber(user.uid) ? user.uid > 0 : activitypub.helpers.isUri(user.uid))); return searchResult; }; @@ -78,12 +92,17 @@ module.exports = function (User) { hardCap = hardCap || resultsPerPage * 10; const data = await db.getSortedSetRangeByLex(`${searchBy}:sorted`, min, max, 0, hardCap); - const uids = data.map(data => data.split(':').pop()); + // const uids = data.map(data => data.split(':').pop()); + const uids = data.map((data) => { + data = data.split(':'); + data.shift(); + return data.join(':'); + }); return uids; } async function filterAndSortUids(uids, data) { - uids = uids.filter(uid => parseInt(uid, 10)); + uids = uids.filter(uid => parseInt(uid, 10) || activitypub.helpers.isUri(uid)); let filters = data.filters || []; filters = Array.isArray(filters) ? filters : [data.filters]; const fields = [];