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.
  • 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