diff --git a/src/activitypub/actors.js b/src/activitypub/actors.js index 9281a28608..d8726c8b9f 100644 --- a/src/activitypub/actors.js +++ b/src/activitypub/actors.js @@ -93,30 +93,39 @@ Actors.assert = async (ids, options = {}) => { 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) => { + const queries = 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 (uidsForCurrent[idx] !== 0) { + memo.searchRemove.push(['ap.preferredUsername:sorted', `${username.toLowerCase()}:${profile.uid}`]); + memo.handleRemove.push(username.toLowerCase()); + } + + memo.searchAdd.push(['ap.preferredUsername:sorted', 0, `${profile.username.toLowerCase()}:${profile.uid}`]); + memo.handleAdd[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}`]); + if (uidsForCurrent[idx] !== 0) { + memo.searchRemove.push(['ap.name:sorted', `${fullname.toLowerCase()}:${profile.uid}`]); + } + + memo.searchAdd.push(['ap.name:sorted', 0, `${profile.fullname.toLowerCase()}:${profile.uid}`]); } } return memo; - }, { remove: [], add: [] }); + }, { searchRemove: [], searchAdd: [], handleRemove: [], handleAdd: {} }); 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), + db.sortedSetAdd('usersRemote:lastCrawled', profiles.map(p => now), profiles.map(p => p.uid)), + db.sortedSetRemoveBulk(queries.searchRemove), + db.sortedSetAddBulk(queries.searchAdd), + db.deleteObjectFields('handle:uid', queries.handleRemove), + db.setObject('handle:uid', queries.handleAdd), ]); return actors.every(Boolean); diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 5f90fce425..50476a93cf 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -235,9 +235,17 @@ Mocks.note = async (post) => { if (matches.size) { tag = tag || []; - Array.from(matches).map(match => ({ - type: 'Mention', - name: match, + tag.push(...Array.from(matches).map(({ id: href, slug: name }) => { + if (utils.isNumber(href)) { // local ref + href = `${nconf.get('url')}/user/${name.slice(1)}`; + name = `${name}@${nconf.get('url_parsed').hostname}`; + } + + return { + type: 'Mention', + href, + name, + }; })); } } diff --git a/src/activitypub/notes.js b/src/activitypub/notes.js index c2beef2055..f513043e7e 100644 --- a/src/activitypub/notes.js +++ b/src/activitypub/notes.js @@ -9,6 +9,7 @@ const privileges = require('../privileges'); const user = require('../user'); const topics = require('../topics'); const posts = require('../posts'); +const plugins = require('../plugins'); const utils = require('../utils'); const activitypub = module.parent.exports; @@ -59,6 +60,9 @@ Notes.assert = async (uid, input, options = {}) => { delete hash._activitypub; // should call internal method here to create/edit post await db.setObject(key, hash); + const post = await posts.getPostData(id); + post._activitypub = postData._activitypub; + plugins.hooks.fire(`action:post.${(exists && options.update) ? 'edit' : 'save'}`, { post }); winston.verbose(`[activitypub/notes.assert] Note ${id} saved.`); } })); @@ -176,17 +180,14 @@ Notes.getParentChain = async (uid, input) => { return chain; }; -Notes.assertParentChain = async (chain, tid) => { +Notes.assertParentChain = async (chain) => { const data = []; chain.reduce((child, parent) => { data.push([`pid:${parent.pid}:replies`, child.timestamp, child.pid]); return parent; }); - await Promise.all([ - db.sortedSetAddBulk(data), - db.setObjectBulk(chain.map(post => [`post:${post.pid}`, { tid }])), - ]); + await db.sortedSetAddBulk(data); }; Notes.assertTopic = async (uid, id) => { @@ -238,7 +239,10 @@ Notes.assertTopic = async (uid, id) => { tid = tid || utils.generateUUID(); mainPost.tid = tid; - const unprocessed = chain.filter((p, idx) => !members[idx]); + const unprocessed = chain.map((post) => { + post.tid = tid; // add tid to post hash + return post; + }).filter((p, idx) => !members[idx]); winston.verbose(`[notes/assertTopic] ${unprocessed.length} new note(s) found.`); const [ids, timestamps] = [ @@ -275,7 +279,7 @@ Notes.assertTopic = async (uid, id) => { Notes.assert(uid, unprocessed), ]); await Promise.all([ // must be done after .assert() - Notes.assertParentChain(chain, tid), + Notes.assertParentChain(chain), Notes.updateTopicCounts(tid), Notes.syncUserInboxes(tid), topics.updateLastPostTimeFromLastPid(tid), diff --git a/src/controllers/posts.js b/src/controllers/posts.js index 7865ba0af7..24e8bdfbb7 100644 --- a/src/controllers/posts.js +++ b/src/controllers/posts.js @@ -4,12 +4,14 @@ const querystring = require('querystring'); const posts = require('../posts'); const privileges = require('../privileges'); +const utils = require('../utils'); + const helpers = require('./helpers'); const postsController = module.exports; postsController.redirectToPost = async function (req, res, next) { - const pid = parseInt(req.params.pid, 10); + const pid = utils.isNumber(req.params.pid) ? parseInt(req.params.pid, 10) : req.params.pid; if (!pid) { return next(); } diff --git a/src/upgrades/4.0.0/searchable_remote_users.js b/src/upgrades/4.0.0/searchable_remote_users.js new file mode 100644 index 0000000000..65b9fceed0 --- /dev/null +++ b/src/upgrades/4.0.0/searchable_remote_users.js @@ -0,0 +1,28 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Allow remote user profiles to be searched', + // remember, month is zero-indexed (so January is 0, December is 11) + timestamp: Date.UTC(2024, 2, 1), + method: async () => { + const ids = await db.getSortedSetMembers('usersRemote:lastCrawled'); + const data = await db.getObjectsFields(ids.map(id => `userRemote:${id}`), ['username', 'fullname']); + + const queries = data.reduce((memo, profile, idx) => { + if (profile && profile.username && profile.fullname) { + memo.zset.push(['ap.preferredUsername:sorted', 0, `${profile.username.toLowerCase()}:${ids[idx]}`]); + memo.zset.push(['ap.name:sorted', 0, `${profile.fullname.toLowerCase()}:${ids[idx]}`]); + memo.hash[profile.username.toLowerCase()] = ids[idx]; + } + + return memo; + }, { zset: [], hash: {} }); + + await Promise.all([ + db.sortedSetAddBulk(queries.zset), + db.setObject('handle:uid', queries.hash), + ]); + }, +}; diff --git a/src/user/index.js b/src/user/index.js index 7a48c2d08b..40beaf069a 100644 --- a/src/user/index.js +++ b/src/user/index.js @@ -111,11 +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; + return (await db.getObjectField('handle:uid', userslug)) || 0; } return await db.sortedSetScore('userslug:uid', userslug);