oft/module/tweaks/chatImageLinks.mjs

125 lines
3 KiB
JavaScript

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 `<img src="${url}" alt="${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