Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • World
  • Users
  • Groups
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse

NodeBB

  1. Home
  2. Bug Reports
  3. Manage Open Social Web handles not consistent after disabled Federation

Manage Open Social Web handles not consistent after disabled Federation

Scheduled Pinned Locked Moved Bug Reports
13 Posts 3 Posters 0 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • J This user is from outside of this forum
    J This user is from outside of this forum
    jasonwch@community.nodebb.org
    wrote on last edited by
    #1

    I've already disabled Federation from ACP on this NodeBB (docker deployed). Register/Login screen no longer have "Manage Open Social Web handles" button. But at topic or category view, click "Log in to reply" or "Log in to Post", still have "Manage Open Social Web handles" link showing up.

    Answer from AI:

    • The modal is rendered from public/templates/modals/intents/trigger.tpl.
    • The link text comes from the language key login:manage-social-web-handles.
    • The click handler is in public/src/modules/intents.js (_intentsHandler), and it is wired up for guest “Log in to reply/post” buttons by:
      public/src/client/topic.js
      public/src/client/category.js
      public/src/client/world.js

    The ACP Federation → Enable ActivityPub toggle (meta.config.activitypubEnabled) already controls the “Manage Open Social Web Handles” button on the /login and /register pages via osw_logins, but the topic/category/world guest modal does not check that flag. That is a core bug.

    julian@community.nodebb.orgJ 1 Reply Last reply
    0
    • J jasonwch@community.nodebb.org

      I've already disabled Federation from ACP on this NodeBB (docker deployed). Register/Login screen no longer have "Manage Open Social Web handles" button. But at topic or category view, click "Log in to reply" or "Log in to Post", still have "Manage Open Social Web handles" link showing up.

      Answer from AI:

      • The modal is rendered from public/templates/modals/intents/trigger.tpl.
      • The link text comes from the language key login:manage-social-web-handles.
      • The click handler is in public/src/modules/intents.js (_intentsHandler), and it is wired up for guest “Log in to reply/post” buttons by:
        public/src/client/topic.js
        public/src/client/category.js
        public/src/client/world.js

      The ACP Federation → Enable ActivityPub toggle (meta.config.activitypubEnabled) already controls the “Manage Open Social Web Handles” button on the /login and /register pages via osw_logins, but the topic/category/world guest modal does not check that flag. That is a core bug.

      julian@community.nodebb.orgJ This user is from outside of this forum
      julian@community.nodebb.orgJ This user is from outside of this forum
      julian@community.nodebb.org
      wrote on last edited by
      #2

      @jasonwch the Activity Intents behaviour is meant to be active even if federation is disabled.

      I'll fix up the behaviour so it is consistent even if federation is disabled.

      1 Reply Last reply
      0
      • J This user is from outside of this forum
        J This user is from outside of this forum
        jasonwch@community.nodebb.org
        wrote on last edited by
        #3

        Thanks so much

        1 Reply Last reply
        0
        • julian@community.nodebb.orgJ This user is from outside of this forum
          julian@community.nodebb.orgJ This user is from outside of this forum
          julian@community.nodebb.org
          wrote on last edited by
          #4

          > 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

          1 Reply Last reply
          0
          • J This user is from outside of this forum
            J This user is from outside of this forum
            jasonwch@community.nodebb.org
            wrote on last edited by
            #5

            @julian Yep, I am temporarily patch the files and inject the code, will wait for the fix from upstream

            1 Reply Last reply
            0
            • J This user is from outside of this forum
              J This user is from outside of this forum
              jasonwch@community.nodebb.org
              wrote on last edited by
              #6

              @baris @julian upvote and downvote also have this issue. Please help to fix as well. Thanks

              1 Reply Last reply
              0
              • J This user is from outside of this forum
                J This user is from outside of this forum
                jasonwch@community.nodebb.org
                wrote on last edited by
                #7

                This patch seems isn't in place with 4.14?

                julian@community.nodebb.orgJ 1 Reply Last reply
                0
                • J jasonwch@community.nodebb.org

                  This patch seems isn't in place with 4.14?

                  julian@community.nodebb.orgJ This user is from outside of this forum
                  julian@community.nodebb.orgJ This user is from outside of this forum
                  julian@community.nodebb.org
                  wrote on last edited by
                  #8

                  @jasonwch no, I javent looked into the issue yet 🫤

                  1 Reply Last reply
                  0
                  • J This user is from outside of this forum
                    J This user is from outside of this forum
                    jasonwch@community.nodebb.org
                    wrote on last edited by
                    #9

                    @julian @baris

                    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]) =&gt; intents &amp;&amp; typeof intents === 'object' &amp;&amp; requiredIntent in intents)
                    		.map(([handle, intents]) =&gt; ({ handle, intents }));
                    
                    	const matchingHandles = await Promise.all(entries.map(async ({ handle, intents }) =&gt; ({
                    		handle,
                    		intents: (await mapIntentNames(intents)).join(', '),
                    	})));
                    
                    	app.parseAndTranslate('modals/intents/trigger', {
                    		displayIntent,
                    		matchingHandles,
                    		hasAnyHandles: map.size &gt; 0,
                    	}, (html) =&gt; {
                    		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 &amp;&amp; intents[requiredIntent];
                    
                    			// Replace template placeholders with URL-encoded parameter values
                    			if (url &amp;&amp; parameters &amp;&amp; typeof parameters === 'object') {
                    				Object.keys(parameters).forEach((prop) =&gt; {
                    					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(() =&gt; register(), 300);
                    		});
                    
                    		// Redirect to login
                    		modal.on('click', '[data-action="go-login"]', function () {
                    			modal.modal('hide');
                    			ajaxify.go('login');
                    		});
                    	});
                    }
                    
                    julian@community.nodebb.orgJ 1 Reply Last reply
                    0
                    • J jasonwch@community.nodebb.org

                      @julian @baris

                      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 &gt;= 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 &amp;&amp; req.useragent.isSafari,
                      		},
                      		fontawesome: {
                      			pro: fontawesome_pro,
                      			styles: fontawesome_styles,
                      			version: fontawesome_version,
                      		},
                      		activitypub: {
                      			enabled: !!meta.config.activitypubEnabled,
                      			probe: meta.config.activitypubEnabled &amp;&amp; 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 &amp;&amp; settings.bootswatchSkin) {
                      		if (settings.bootswatchSkin === 'noskin') {
                      			config.bootswatchSkin = '';
                      		} else if (settings.bootswatchSkin !== '' &amp;&amp; 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 =&gt; `${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 =&gt; {
                      			if (entry &amp;&amp; entry.handle &amp;&amp; typeof entry.intents === 'object' &amp;&amp; !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]) =&gt; ({ 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 &amp;&amp; result.intents &amp;&amp; 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]) =&gt; ({
                      		handle,
                      		intents: (await mapIntentNames(intents)).join(', '),
                      	})));
                      
                      	app.parseAndTranslate('modals/intents/register', {
                      		description: '[[intents:description]]',
                      		handles,
                      	}, (html) =&gt; {
                      		const modal = dialog({
                      			title: '[[intents:title]]',
                      			message: html,
                      		});
                      
                      		const handleInput = modal.find('#intents-handle-input');
                      		const submitBtn = modal.find('#intents-register-btn');
                      
                      		const validateHandle = () =&gt; {
                      			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) =&gt; {
                      			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]) =&gt; ({
                      					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]) =&gt; ({ 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]) =&gt; intents &amp;&amp; typeof intents === 'object' &amp;&amp; requiredIntent in intents)
                      		.map(([handle, intents]) =&gt; ({ handle, intents }));
                      
                      	const matchingHandles = await Promise.all(entries.map(async ({ handle, intents }) =&gt; ({
                      		handle,
                      		intents: (await mapIntentNames(intents)).join(', '),
                      	})));
                      
                      	app.parseAndTranslate('modals/intents/trigger', {
                      		displayIntent,
                      		matchingHandles,
                      		hasAnyHandles: map.size &gt; 0,
                      	}, (html) =&gt; {
                      		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 &amp;&amp; intents[requiredIntent];
                      
                      			// Replace template placeholders with URL-encoded parameter values
                      			if (url &amp;&amp; parameters &amp;&amp; typeof parameters === 'object') {
                      				Object.keys(parameters).forEach((prop) =&gt; {
                      					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(() =&gt; register(), 300);
                      		});
                      
                      		// Redirect to login
                      		modal.on('click', '[data-action="go-login"]', function () {
                      			modal.modal('hide');
                      			ajaxify.go('login');
                      		});
                      	});
                      }
                      
                      julian@community.nodebb.orgJ This user is from outside of this forum
                      julian@community.nodebb.orgJ This user is from outside of this forum
                      julian@community.nodebb.org
                      wrote on last edited by
                      #10

                      @jasonwch latest NodeBB should have this patched already, thanks.

                      1 Reply Last reply
                      0
                      • informapirata@community.nodebb.orgI This user is from outside of this forum
                        informapirata@community.nodebb.orgI This user is from outside of this forum
                        informapirata@community.nodebb.org
                        wrote on last edited by
                        #11

                        > @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="😵" />

                        what-the-fuck-gif-2.gif

                        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?"

                        1 Reply Last reply
                        0
                        • J This user is from outside of this forum
                          J This user is from outside of this forum
                          jasonwch@community.nodebb.org
                          wrote on last edited by
                          #12

                          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

                          julian@community.nodebb.orgJ 1 Reply Last reply
                          0
                          • J jasonwch@community.nodebb.org

                            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

                            julian@community.nodebb.orgJ This user is from outside of this forum
                            julian@community.nodebb.orgJ This user is from outside of this forum
                            julian@community.nodebb.org
                            wrote on last edited by
                            #13

                            @jasonwch try latest develop branch if you don't want to wait for 4.15

                            1 Reply Last reply
                            0

                            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
                            Reply
                            • Reply as topic
                            Log in to reply
                            • Oldest to Newest
                            • Newest to Oldest
                            • Most Votes


                            • Login

                            • Login or register to search.
                            Powered by NodeBB Contributors
                            • First post
                              Last post
                            0
                            • Categories
                            • Recent
                            • Tags
                            • Popular
                            • World
                            • Users
                            • Groups