import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; const key = `chatImageLinks`; const IMAGE_TYPES = [ `png`, `jpg`, `jpeg`, `webp`, `svg`, ]; export function chatImageLinks() { status[key] = SettingStatusEnum.Unknown; if (preventTweakRegistration(key)) { return }; /** @type {number|null} */ let hookID = null; // #region Registration Logger.log(`Registering setting: ${key}`); game.settings.register(__ID__, key, { name: `OFT.setting.${key}.name`, hint: `OFT.setting.${key}.hint`, scope: `user`, type: Boolean, default: true, config: true, requiresReload: false, onChange: (newValue) => { if (newValue) { hookID = Hooks.on(`chatMessage`, chatMessageHandler); } else if (hookID != null) { Hooks.off(`chatMessage`, hookID); }; }, }); // #endregion Registration // #region Implementation if (game.settings.get(__ID__, key)) { Logger.log(`setting:${key} | Adding chat message listener`); Hooks.on(`chatMessage`, chatMessageHandler); }; // #endregion Implementation status[key] = SettingStatusEnum.Registered; }; // #region Helpers const pattern = new RegExp( `https?:\\/\\/\\S*\\.(?:${IMAGE_TYPES.join(`|`)})`, `gi`, ); // MARK: Mutate & Resend const handled = new Set(); async function mutateAndResendMessage(chatLog, message, options) { const validMatches = new Set(); const matches = message.match(pattern); for (const match of matches) { if (await isAcceptableImage(match)) { validMatches.add(match); }; }; message = message.replaceAll( pattern, (url) => { if (!validMatches.has(url)) { return url; }; return `${url}`; }, ); handled.add(message); chatLog.processMessage(message, options); }; // MARK: Chat Message /** * Must be synchronous since it is a hook handler, but the mutation + * resending can be done asynchronously since it doesn't matter how * long it takes. */ function chatMessageHandler(chatLog, message, options) { if (!game.settings.get(__ID__, key)) { return }; // Don't re-process the same message if (handled.has(message)) { return }; // Prevent cancellation for non-matching messages const match = message.match(pattern); if (!match) { return }; mutateAndResendMessage(chatLog, message, options); return false; }; // MARK: isAcceptableImage async function isAcceptableImage(url) { if (!URL.canParse(url)) { return false }; try { const response = await fetch(url, { method: `HEAD` }); const contentType = response.headers.get(`Content-Type`); Logger.debug(`Image data:`, { url, contentType }); let [ superType, subtype ] = contentType.split(`/`); if (superType !== `image`) { return false; }; if (subtype.includes(`+`)) { subtype = subtype.split(`+`, 2).at(0); }; return IMAGE_TYPES.includes(subtype); } catch { return false; }; }; // #endregion Helpers