Manage Open Social Web handles not consistent after disabled Federation
-
> julian said:
>
> the Activity Intents behaviour is meant to be active even if federation is disabled.Actually, you're right, it's not meant to work without federation
-
@julian Yep, I am temporarily patch the files and inject the code, will wait for the fix from upstream
-
-
This patch seems isn't in place with 4.14?
-
This patch seems isn't in place with 4.14?
@jasonwch no, I javent looked into the issue yet 🫤
-
Hope this help, the following 2 files are patched by me at 4.13.2. Hope this can be adjust with 4.14 then merge to 4.14.1
// location: api.js 'use strict'; const validator = require('validator'); const nconf = require('nconf'); const meta = require('../meta'); const user = require('../user'); const categories = require('../categories'); const plugins = require('../plugins'); const translator = require('../translator'); const languages = require('../languages'); const { generateToken } = require('../middleware/csrf'); const utils = require('../utils'); const apiController = module.exports; const url = nconf.get('url'); const relative_path = nconf.get('relative_path'); const upload_url = nconf.get('upload_url'); const asset_base_url = nconf.get('asset_base_url'); const socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket']; const socketioOrigins = nconf.get('socket.io:origins'); const websocketAddress = nconf.get('socket.io:address') || ''; const fontawesome_pro = nconf.get('fontawesome:pro') || false; const fontawesome_styles = utils.getFontawesomeStyles(); const fontawesome_version = utils.getFontawesomeVersion(); apiController.loadConfig = async function (req) { const config = { url, relative_path, upload_url, asset_base_url, assetBaseUrl: asset_base_url, // deprecate in 1.20.x siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')), browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')), description: validator.escape(String(meta.config.description || '')), keywords: validator.escape(String(meta.config.keywords || '')), 'brand:logo': validator.escape(String(meta.config['brand:logo'])), titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'), showSiteTitle: meta.config.showSiteTitle === 1, maintenanceMode: meta.config.maintenanceMode === 1, postQueue: meta.config.postQueue, minimumTitleLength: meta.config.minimumTitleLength, maximumTitleLength: meta.config.maximumTitleLength, minimumPostLength: meta.config.minimumPostLength, maximumPostLength: meta.config.maximumPostLength, minimumTagsPerTopic: meta.config.minimumTagsPerTopic || 0, maximumTagsPerTopic: meta.config.maximumTagsPerTopic || 5, minimumTagLength: meta.config.minimumTagLength || 3, maximumTagLength: meta.config.maximumTagLength || 15, undoTimeout: meta.config.undoTimeout || 0, useOutgoingLinksPage: meta.config.useOutgoingLinksPage === 1, outgoingLinksWhitelist: meta.config.useOutgoingLinksPage === 1 ? meta.config['outgoingLinks:whitelist'] : undefined, allowGuestHandles: meta.config.allowGuestHandles === 1, allowTopicsThumbnail: meta.config.allowTopicsThumbnail === 1, usePagination: meta.config.usePagination === 1, disableChat: meta.config.disableChat === 1, disableChatMessageEditing: meta.config.disableChatMessageEditing === 1, maximumChatMessageLength: meta.config.maximumChatMessageLength || 1000, socketioTransports, socketioOrigins, websocketAddress, maxReconnectionAttempts: meta.config.maxReconnectionAttempts, reconnectionDelay: meta.config.reconnectionDelay, topicsPerPage: meta.config.topicsPerPage || 20, postsPerPage: meta.config.postsPerPage || 20, maximumFileSize: meta.config.maximumFileSize, convertPastedImageTo: meta.config.convertPastedImageTo, 'theme:id': meta.config['theme:id'], 'theme:src': meta.config['theme:src'], defaultLang: meta.config.defaultLang || 'en-GB', userLang: req.query.lang ? validator.escape(String(req.query.lang)) : (meta.config.defaultLang || 'en-GB'), loggedIn: !!req.user, uid: req.uid, 'cache-buster': meta.config['cache-buster'] || '', topicPostSort: meta.config.topicPostSort || 'oldest_to_newest', categoryTopicSort: meta.config.categoryTopicSort || 'recently_replied', csrf_token: req.uid >= 0 ? generateToken(req) : false, searchEnabled: plugins.hooks.hasListeners('filter:search.query'), searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', bootswatchSkin: meta.config.bootswatchSkin || '', 'composer:showHelpTab': meta.config['composer:showHelpTab'] === 1, enablePostHistory: meta.config.enablePostHistory === 1, timeagoCutoff: meta.config.timeagoCutoff !== '' ? Math.max(0, parseInt(meta.config.timeagoCutoff, 10)) : meta.config.timeagoCutoff, timeagoCodes: languages.timeagoCodes, cookies: { enabled: meta.config.cookieConsentEnabled === 1, message: translator.escape(validator.escape(meta.config.cookieConsentMessage || '[[global:cookies.message]]')).replace(/\\/g, '\\\\'), dismiss: translator.escape(validator.escape(meta.config.cookieConsentDismiss || '[[global:cookies.accept]]')).replace(/\\/g, '\\\\'), link: translator.escape(validator.escape(meta.config.cookieConsentLink || '[[global:cookies.learn-more]]')).replace(/\\/g, '\\\\'), link_url: translator.escape(validator.escape(meta.config.cookieConsentLinkUrl || 'https://www.cookiesandyou.com')).replace(/\\/g, '\\\\'), }, thumbs: { size: meta.config.topicThumbSize, }, emailPrompt: meta.config.emailPrompt, useragent: { isSafari: req.useragent && req.useragent.isSafari, }, fontawesome: { pro: fontawesome_pro, styles: fontawesome_styles, version: fontawesome_version, }, activitypub: { enabled: !!meta.config.activitypubEnabled, probe: meta.config.activitypubEnabled && meta.config.activitypubProbe, worldDefaultCid: meta.config.activitypubWorldDefaultCid, }, tinycon: { color: meta.config.tinyconColor, background: meta.config.tinyconBackground, }, }; let settings = config; let isAdminOrGlobalMod; if (req.loggedIn) { ([settings, isAdminOrGlobalMod] = await Promise.all([ user.getSettings(req.uid), user.isAdminOrGlobalMod(req.uid), ])); } // Handle old skin configs const oldSkins = ['default']; settings.bootswatchSkin = oldSkins.includes(settings.bootswatchSkin) ? '' : settings.bootswatchSkin; config.usePagination = settings.usePagination; config.topicsPerPage = settings.topicsPerPage; config.postsPerPage = settings.postsPerPage; config.userLang = validator.escape( String((req.query.lang ? req.query.lang : null) || settings.userLang || config.defaultLang) ); config.acpLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.acpLang)); config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; config.topicPostSort = settings.topicPostSort || config.topicPostSort; config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.topicSearchEnabled = settings.topicSearchEnabled || false; config.disableCustomUserSkins = meta.config.disableCustomUserSkins === 1; config.defaultBootswatchSkin = config.bootswatchSkin; if (!config.disableCustomUserSkins && settings.bootswatchSkin) { if (settings.bootswatchSkin === 'noskin') { config.bootswatchSkin = ''; } else if (settings.bootswatchSkin !== '' && await meta.css.isSkinValid(settings.bootswatchSkin)) { config.bootswatchSkin = settings.bootswatchSkin; } } config.hideReadNotifications = settings.hideReadNotifications; // Overrides based on privilege config.disableChatMessageEditing = isAdminOrGlobalMod ? false : config.disableChatMessageEditing; return await plugins.hooks.fire('filter:config.get', config); }; apiController.getConfig = async function (req, res) { const config = await apiController.loadConfig(req); res.json(config); }; apiController.getModerators = async function (req, res) { const moderators = await categories.getModerators(req.params.cid); res.json({ moderators: moderators }); }; require('../promisify')(apiController, ['getConfig', 'getObject', 'getModerators']);// Location: intents.js 'use strict'; import { dialog } from 'bootbox'; import { get } from 'api'; import storage from 'storage'; import { translate, translateKeys } from 'translator'; import * as alerts from './alerts'; const STORAGE_KEY = 'ap:intents:handles'; function getStoredData() { try { return storage.getItem(STORAGE_KEY); } catch (e) { return null; } } function setStoredData(data) { try { storage.setItem(STORAGE_KEY, data); } catch (e) { // Storage full or unavailable — silently fail } } const INTENT_DISPLAY_MAP = { create: '[[intents:display.create]]', like: '[[intents:display.like]]', dislike: '[[intents:display.dislike]]', follow: '[[intents:display.follow]]', object: '[[intents:display.object]]', }; async function mapIntentNames(intents) { return await translateKeys(Object.keys(intents).map(intent => `${INTENT_DISPLAY_MAP[intent.toLowerCase()]}`)); } export function list() { const raw = getStoredData(); if (!raw) { return new Map(); } let handles; try { handles = JSON.parse(raw); } catch (e) { // Corrupt data — reset return new Map(); } const map = new Map(); if (Array.isArray(handles)) { handles.forEach(entry => { if (entry && entry.handle && typeof entry.intents === 'object' && !Array.isArray(entry.intents)) { map.set(entry.handle, entry.intents); } }); } return map; } export function save(handle, intents) { if (typeof handle !== 'string' || !handle.trim()) { return; } handle = handle.trim(); if (!intents || typeof intents !== 'object' || Array.isArray(intents)) { return; } const map = list(); map.set(handle, intents); const entries = Array.from(map.entries()).map(([h, i]) => ({ handle: h, intents: i })); setStoredData(JSON.stringify(entries)); } export async function refresh(handle) { if (typeof handle !== 'string' || !handle.trim()) { return null; } handle = handle.trim().replace(/^@/, ''); const result = await get(`/api/v3/intents/query/${handle}`); if (result && result.intents && typeof result.intents === 'object') { save(handle, result.intents); return { intents: Object.keys(result.intents) }; } return null; } export async function register() { let map = list(); let handles = await Promise.all(Array.from(map.entries()).map(async ([handle, intents]) => ({ handle, intents: (await mapIntentNames(intents)).join(', '), }))); app.parseAndTranslate('modals/intents/register', { description: '[[intents:description]]', handles, }, (html) => { const modal = dialog({ title: '[[intents:title]]', message: html, }); const handleInput = modal.find('#intents-handle-input'); const submitBtn = modal.find('#intents-register-btn'); const validateHandle = () => { const val = handleInput.val().trim(); // Validate: must be in format @username@domain or username@domain const valid = /^@?[\w.-]+@[\w.-]+\.[\w]{2,}$/.test(val); submitBtn.prop('disabled', !valid); }; handleInput.on('input', validateHandle); validateHandle(); modal.find('#intents-register-form').on('submit', async (ev) => { ev.preventDefault(); const handle = handleInput.val().trim(); submitBtn.prop('disabled', true); try { await refresh(handle); map = list(); handles = await Promise.all(Array.from(map.entries()).map(async ([handle, intents]) => ({ handle, intents: (await mapIntentNames(intents)).join(', '), }))); const html = await app.parseAndTranslate('modals/intents/register', 'handles', { handles }); modal.find('#intents-registered-list').html(html); } catch (e) { alerts.error(e.message); } finally { handleInput.val(''); submitBtn.prop('disabled', false); } }); modal.on('click', '[data-action="remove"]', function () { const handleToRemove = $(this).attr('data-handle'); const map = list(); map.delete(handleToRemove); const entries = Array.from(map.entries()).map(([h, i]) => ({ handle: h, intents: i })); setStoredData(JSON.stringify(entries)); $(this).closest('li').remove(); if (!map.size) { modal.find('#intents-registered-list').closest('hr').next('h6, p').remove(); modal.find('#intents-registered-list').closest('hr').prev('p').after('<p class="text-muted mt-3">[[intents:no-handles]]</p>'); } }); }); } const INTENTS_GUEST_SELECTORS = '[component="topic/reply/guest"], [component="category/post/guest"]'; // called by various init scripts in different pages' js to add handlers for "Log in to post" buttons, et al. export function addHandlers() { document.removeEventListener('click', _intentsHandler); document.addEventListener('click', _intentsHandler, true); // capture phase } function _intentsHandler(e) { if (!config.activitypub || !config.activitypub.enabled) { return; } const target = e.target.closest(INTENTS_GUEST_SELECTORS); if (target) { e.preventDefault(); e.stopPropagation(); const tid = ajaxify.data.tid; const cid = ajaxify.data.cid; const payload = {}; if (tid) { payload.inReplyTo = utils.isNumber(ajaxify.data.mainPid) ? `${config.url}/post/${ajaxify.data.mainPid}` : ajaxify.data.mainPid; payload.content = `@${ajaxify.data.author.userslug}`; } else if (cid) { payload.content = `@${ajaxify.data.handleFull}`; } trigger('create', payload); } } export async function trigger(intent, parameters) { if (!config.activitypub || !config.activitypub.enabled) { ajaxify.go('login'); return; } const map = list(); const requiredIntent = intent.toLowerCase(); const displayKey = INTENT_DISPLAY_MAP[requiredIntent]; const displayIntent = (await translate(`${displayKey || intent}`)); const entries = Array.from(map.entries()) .filter(([, intents]) => intents && typeof intents === 'object' && requiredIntent in intents) .map(([handle, intents]) => ({ handle, intents })); const matchingHandles = await Promise.all(entries.map(async ({ handle, intents }) => ({ handle, intents: (await mapIntentNames(intents)).join(', '), }))); app.parseAndTranslate('modals/intents/trigger', { displayIntent, matchingHandles, hasAnyHandles: map.size > 0, }, (html) => { const modal = dialog({ title: `intents:trigger-title, ${displayIntent}`, message: html, }); // Handle intent execution from a registered handle modal.on('click', '[data-action="execute-intent"]', function () { const handle = $(this).attr('data-handle'); const intents = map.get(handle); let url = intents && intents[requiredIntent]; // Replace template placeholders with URL-encoded parameter values if (url && parameters && typeof parameters === 'object') { Object.keys(parameters).forEach((prop) => { const value = parameters[prop]; const match = `{${prop}}`; if (url.includes(match)) { url = url.replaceAll(match, encodeURIComponent(value)); } }); } // Remove any unmatched placeholders url = url?.replaceAll(/\{[^}]+\}/g, ''); if (url) { // Validate URL scheme to prevent XSS via javascript: data: etc. try { const parsed = new URL(url); if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { window.location.href = url; } } catch (e) { // Invalid URL, silently ignore } } }); // Open handle registration modal modal.on('click', '[data-action="open-register"]', function () { modal.modal('hide'); setTimeout(() => register(), 300); }); // Redirect to login modal.on('click', '[data-action="go-login"]', function () { modal.modal('hide'); ajaxify.go('login'); }); }); } -
Hope this help, the following 2 files are patched by me at 4.13.2. Hope this can be adjust with 4.14 then merge to 4.14.1
// location: api.js 'use strict'; const validator = require('validator'); const nconf = require('nconf'); const meta = require('../meta'); const user = require('../user'); const categories = require('../categories'); const plugins = require('../plugins'); const translator = require('../translator'); const languages = require('../languages'); const { generateToken } = require('../middleware/csrf'); const utils = require('../utils'); const apiController = module.exports; const url = nconf.get('url'); const relative_path = nconf.get('relative_path'); const upload_url = nconf.get('upload_url'); const asset_base_url = nconf.get('asset_base_url'); const socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket']; const socketioOrigins = nconf.get('socket.io:origins'); const websocketAddress = nconf.get('socket.io:address') || ''; const fontawesome_pro = nconf.get('fontawesome:pro') || false; const fontawesome_styles = utils.getFontawesomeStyles(); const fontawesome_version = utils.getFontawesomeVersion(); apiController.loadConfig = async function (req) { const config = { url, relative_path, upload_url, asset_base_url, assetBaseUrl: asset_base_url, // deprecate in 1.20.x siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')), browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')), description: validator.escape(String(meta.config.description || '')), keywords: validator.escape(String(meta.config.keywords || '')), 'brand:logo': validator.escape(String(meta.config['brand:logo'])), titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'), showSiteTitle: meta.config.showSiteTitle === 1, maintenanceMode: meta.config.maintenanceMode === 1, postQueue: meta.config.postQueue, minimumTitleLength: meta.config.minimumTitleLength, maximumTitleLength: meta.config.maximumTitleLength, minimumPostLength: meta.config.minimumPostLength, maximumPostLength: meta.config.maximumPostLength, minimumTagsPerTopic: meta.config.minimumTagsPerTopic || 0, maximumTagsPerTopic: meta.config.maximumTagsPerTopic || 5, minimumTagLength: meta.config.minimumTagLength || 3, maximumTagLength: meta.config.maximumTagLength || 15, undoTimeout: meta.config.undoTimeout || 0, useOutgoingLinksPage: meta.config.useOutgoingLinksPage === 1, outgoingLinksWhitelist: meta.config.useOutgoingLinksPage === 1 ? meta.config['outgoingLinks:whitelist'] : undefined, allowGuestHandles: meta.config.allowGuestHandles === 1, allowTopicsThumbnail: meta.config.allowTopicsThumbnail === 1, usePagination: meta.config.usePagination === 1, disableChat: meta.config.disableChat === 1, disableChatMessageEditing: meta.config.disableChatMessageEditing === 1, maximumChatMessageLength: meta.config.maximumChatMessageLength || 1000, socketioTransports, socketioOrigins, websocketAddress, maxReconnectionAttempts: meta.config.maxReconnectionAttempts, reconnectionDelay: meta.config.reconnectionDelay, topicsPerPage: meta.config.topicsPerPage || 20, postsPerPage: meta.config.postsPerPage || 20, maximumFileSize: meta.config.maximumFileSize, convertPastedImageTo: meta.config.convertPastedImageTo, 'theme:id': meta.config['theme:id'], 'theme:src': meta.config['theme:src'], defaultLang: meta.config.defaultLang || 'en-GB', userLang: req.query.lang ? validator.escape(String(req.query.lang)) : (meta.config.defaultLang || 'en-GB'), loggedIn: !!req.user, uid: req.uid, 'cache-buster': meta.config['cache-buster'] || '', topicPostSort: meta.config.topicPostSort || 'oldest_to_newest', categoryTopicSort: meta.config.categoryTopicSort || 'recently_replied', csrf_token: req.uid >= 0 ? generateToken(req) : false, searchEnabled: plugins.hooks.hasListeners('filter:search.query'), searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', bootswatchSkin: meta.config.bootswatchSkin || '', 'composer:showHelpTab': meta.config['composer:showHelpTab'] === 1, enablePostHistory: meta.config.enablePostHistory === 1, timeagoCutoff: meta.config.timeagoCutoff !== '' ? Math.max(0, parseInt(meta.config.timeagoCutoff, 10)) : meta.config.timeagoCutoff, timeagoCodes: languages.timeagoCodes, cookies: { enabled: meta.config.cookieConsentEnabled === 1, message: translator.escape(validator.escape(meta.config.cookieConsentMessage || '[[global:cookies.message]]')).replace(/\\/g, '\\\\'), dismiss: translator.escape(validator.escape(meta.config.cookieConsentDismiss || '[[global:cookies.accept]]')).replace(/\\/g, '\\\\'), link: translator.escape(validator.escape(meta.config.cookieConsentLink || '[[global:cookies.learn-more]]')).replace(/\\/g, '\\\\'), link_url: translator.escape(validator.escape(meta.config.cookieConsentLinkUrl || 'https://www.cookiesandyou.com')).replace(/\\/g, '\\\\'), }, thumbs: { size: meta.config.topicThumbSize, }, emailPrompt: meta.config.emailPrompt, useragent: { isSafari: req.useragent && req.useragent.isSafari, }, fontawesome: { pro: fontawesome_pro, styles: fontawesome_styles, version: fontawesome_version, }, activitypub: { enabled: !!meta.config.activitypubEnabled, probe: meta.config.activitypubEnabled && meta.config.activitypubProbe, worldDefaultCid: meta.config.activitypubWorldDefaultCid, }, tinycon: { color: meta.config.tinyconColor, background: meta.config.tinyconBackground, }, }; let settings = config; let isAdminOrGlobalMod; if (req.loggedIn) { ([settings, isAdminOrGlobalMod] = await Promise.all([ user.getSettings(req.uid), user.isAdminOrGlobalMod(req.uid), ])); } // Handle old skin configs const oldSkins = ['default']; settings.bootswatchSkin = oldSkins.includes(settings.bootswatchSkin) ? '' : settings.bootswatchSkin; config.usePagination = settings.usePagination; config.topicsPerPage = settings.topicsPerPage; config.postsPerPage = settings.postsPerPage; config.userLang = validator.escape( String((req.query.lang ? req.query.lang : null) || settings.userLang || config.defaultLang) ); config.acpLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.acpLang)); config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; config.topicPostSort = settings.topicPostSort || config.topicPostSort; config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.topicSearchEnabled = settings.topicSearchEnabled || false; config.disableCustomUserSkins = meta.config.disableCustomUserSkins === 1; config.defaultBootswatchSkin = config.bootswatchSkin; if (!config.disableCustomUserSkins && settings.bootswatchSkin) { if (settings.bootswatchSkin === 'noskin') { config.bootswatchSkin = ''; } else if (settings.bootswatchSkin !== '' && await meta.css.isSkinValid(settings.bootswatchSkin)) { config.bootswatchSkin = settings.bootswatchSkin; } } config.hideReadNotifications = settings.hideReadNotifications; // Overrides based on privilege config.disableChatMessageEditing = isAdminOrGlobalMod ? false : config.disableChatMessageEditing; return await plugins.hooks.fire('filter:config.get', config); }; apiController.getConfig = async function (req, res) { const config = await apiController.loadConfig(req); res.json(config); }; apiController.getModerators = async function (req, res) { const moderators = await categories.getModerators(req.params.cid); res.json({ moderators: moderators }); }; require('../promisify')(apiController, ['getConfig', 'getObject', 'getModerators']);// Location: intents.js 'use strict'; import { dialog } from 'bootbox'; import { get } from 'api'; import storage from 'storage'; import { translate, translateKeys } from 'translator'; import * as alerts from './alerts'; const STORAGE_KEY = 'ap:intents:handles'; function getStoredData() { try { return storage.getItem(STORAGE_KEY); } catch (e) { return null; } } function setStoredData(data) { try { storage.setItem(STORAGE_KEY, data); } catch (e) { // Storage full or unavailable — silently fail } } const INTENT_DISPLAY_MAP = { create: '[[intents:display.create]]', like: '[[intents:display.like]]', dislike: '[[intents:display.dislike]]', follow: '[[intents:display.follow]]', object: '[[intents:display.object]]', }; async function mapIntentNames(intents) { return await translateKeys(Object.keys(intents).map(intent => `${INTENT_DISPLAY_MAP[intent.toLowerCase()]}`)); } export function list() { const raw = getStoredData(); if (!raw) { return new Map(); } let handles; try { handles = JSON.parse(raw); } catch (e) { // Corrupt data — reset return new Map(); } const map = new Map(); if (Array.isArray(handles)) { handles.forEach(entry => { if (entry && entry.handle && typeof entry.intents === 'object' && !Array.isArray(entry.intents)) { map.set(entry.handle, entry.intents); } }); } return map; } export function save(handle, intents) { if (typeof handle !== 'string' || !handle.trim()) { return; } handle = handle.trim(); if (!intents || typeof intents !== 'object' || Array.isArray(intents)) { return; } const map = list(); map.set(handle, intents); const entries = Array.from(map.entries()).map(([h, i]) => ({ handle: h, intents: i })); setStoredData(JSON.stringify(entries)); } export async function refresh(handle) { if (typeof handle !== 'string' || !handle.trim()) { return null; } handle = handle.trim().replace(/^@/, ''); const result = await get(`/api/v3/intents/query/${handle}`); if (result && result.intents && typeof result.intents === 'object') { save(handle, result.intents); return { intents: Object.keys(result.intents) }; } return null; } export async function register() { let map = list(); let handles = await Promise.all(Array.from(map.entries()).map(async ([handle, intents]) => ({ handle, intents: (await mapIntentNames(intents)).join(', '), }))); app.parseAndTranslate('modals/intents/register', { description: '[[intents:description]]', handles, }, (html) => { const modal = dialog({ title: '[[intents:title]]', message: html, }); const handleInput = modal.find('#intents-handle-input'); const submitBtn = modal.find('#intents-register-btn'); const validateHandle = () => { const val = handleInput.val().trim(); // Validate: must be in format @username@domain or username@domain const valid = /^@?[\w.-]+@[\w.-]+\.[\w]{2,}$/.test(val); submitBtn.prop('disabled', !valid); }; handleInput.on('input', validateHandle); validateHandle(); modal.find('#intents-register-form').on('submit', async (ev) => { ev.preventDefault(); const handle = handleInput.val().trim(); submitBtn.prop('disabled', true); try { await refresh(handle); map = list(); handles = await Promise.all(Array.from(map.entries()).map(async ([handle, intents]) => ({ handle, intents: (await mapIntentNames(intents)).join(', '), }))); const html = await app.parseAndTranslate('modals/intents/register', 'handles', { handles }); modal.find('#intents-registered-list').html(html); } catch (e) { alerts.error(e.message); } finally { handleInput.val(''); submitBtn.prop('disabled', false); } }); modal.on('click', '[data-action="remove"]', function () { const handleToRemove = $(this).attr('data-handle'); const map = list(); map.delete(handleToRemove); const entries = Array.from(map.entries()).map(([h, i]) => ({ handle: h, intents: i })); setStoredData(JSON.stringify(entries)); $(this).closest('li').remove(); if (!map.size) { modal.find('#intents-registered-list').closest('hr').next('h6, p').remove(); modal.find('#intents-registered-list').closest('hr').prev('p').after('<p class="text-muted mt-3">[[intents:no-handles]]</p>'); } }); }); } const INTENTS_GUEST_SELECTORS = '[component="topic/reply/guest"], [component="category/post/guest"]'; // called by various init scripts in different pages' js to add handlers for "Log in to post" buttons, et al. export function addHandlers() { document.removeEventListener('click', _intentsHandler); document.addEventListener('click', _intentsHandler, true); // capture phase } function _intentsHandler(e) { if (!config.activitypub || !config.activitypub.enabled) { return; } const target = e.target.closest(INTENTS_GUEST_SELECTORS); if (target) { e.preventDefault(); e.stopPropagation(); const tid = ajaxify.data.tid; const cid = ajaxify.data.cid; const payload = {}; if (tid) { payload.inReplyTo = utils.isNumber(ajaxify.data.mainPid) ? `${config.url}/post/${ajaxify.data.mainPid}` : ajaxify.data.mainPid; payload.content = `@${ajaxify.data.author.userslug}`; } else if (cid) { payload.content = `@${ajaxify.data.handleFull}`; } trigger('create', payload); } } export async function trigger(intent, parameters) { if (!config.activitypub || !config.activitypub.enabled) { ajaxify.go('login'); return; } const map = list(); const requiredIntent = intent.toLowerCase(); const displayKey = INTENT_DISPLAY_MAP[requiredIntent]; const displayIntent = (await translate(`${displayKey || intent}`)); const entries = Array.from(map.entries()) .filter(([, intents]) => intents && typeof intents === 'object' && requiredIntent in intents) .map(([handle, intents]) => ({ handle, intents })); const matchingHandles = await Promise.all(entries.map(async ({ handle, intents }) => ({ handle, intents: (await mapIntentNames(intents)).join(', '), }))); app.parseAndTranslate('modals/intents/trigger', { displayIntent, matchingHandles, hasAnyHandles: map.size > 0, }, (html) => { const modal = dialog({ title: `intents:trigger-title, ${displayIntent}`, message: html, }); // Handle intent execution from a registered handle modal.on('click', '[data-action="execute-intent"]', function () { const handle = $(this).attr('data-handle'); const intents = map.get(handle); let url = intents && intents[requiredIntent]; // Replace template placeholders with URL-encoded parameter values if (url && parameters && typeof parameters === 'object') { Object.keys(parameters).forEach((prop) => { const value = parameters[prop]; const match = `{${prop}}`; if (url.includes(match)) { url = url.replaceAll(match, encodeURIComponent(value)); } }); } // Remove any unmatched placeholders url = url?.replaceAll(/\{[^}]+\}/g, ''); if (url) { // Validate URL scheme to prevent XSS via javascript: data: etc. try { const parsed = new URL(url); if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { window.location.href = url; } } catch (e) { // Invalid URL, silently ignore } } }); // Open handle registration modal modal.on('click', '[data-action="open-register"]', function () { modal.modal('hide'); setTimeout(() => register(), 300); }); // Redirect to login modal.on('click', '[data-action="go-login"]', function () { modal.modal('hide'); ajaxify.go('login'); }); }); }@jasonwch latest NodeBB should have this patched already, thanks.
-
> @julian said:
>
> the Activity Intents behaviour# EEEHHH? WHAT THE FUCK IS THAT BUTTON?!?! <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f635.png?v=32e548a362d" title="
" /> <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f635.png?v=32e548a362d" title="
" /> <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f635.png?v=32e548a362d" title="
" />
Uhm... I meant to say:
"Oops, I'm the administrator of a federated NodeBB server and I've never noticed that button, nor did I understand how to use it. Where can I find documentation that explains how it works?"
-
https://github.com/NodeBB/NodeBB/issues/14396
It's still not at 4.14.2. all reply, post, upvote still open social prompt even though ferderation is off
-
https://github.com/NodeBB/NodeBB/issues/14396
It's still not at 4.14.2. all reply, post, upvote still open social prompt even though ferderation is off
@jasonwch try latest develop branch if you don't want to wait for 4.15
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login