const MAX_CONDITIONAL_REPLIES = 100; function normalizeConditionalKey(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^\p{L}\p{N}]+/gu, " ") .replace(/\s+/g, " ") .trim(); } function splitConditionalKeywords(value) { return String(value || "") .split(",") .map((keyword) => keyword.trim().slice(0, 100)) .filter(Boolean); } function normalizeConditionalReplies(value) { return normalizeConditionalReplyRows(value).flatMap((entry) => splitConditionalKeywords(entry.keyword).map((keyword) => ({ keyword, normalized: normalizeConditionalKey(keyword), response: entry.response }))); } function normalizeConditionalReplyRows(value) { let input = value; if (typeof input === "string") { try { input = JSON.parse(input); } catch { input = []; } } if (!Array.isArray(input)) return []; const seen = new Set(); const rows = []; let aliasCount = 0; for (const entry of input) { const response = String(entry?.response || "").trim().slice(0, 4000); if (!response) continue; const aliases = []; for (const keyword of splitConditionalKeywords(entry?.keyword)) { const normalized = normalizeConditionalKey(keyword); if (!normalized || seen.has(normalized)) continue; seen.add(normalized); aliases.push(keyword); aliasCount += 1; if (aliasCount >= MAX_CONDITIONAL_REPLIES) break; } if (aliases.length) rows.push({ keyword: aliases.join(", "), response }); if (aliasCount >= MAX_CONDITIONAL_REPLIES) break; } return rows; } function conditionalRepliesFromBody(body = {}) { const keywords = asArray(body.conditional_keyword); const responses = asArray(body.conditional_response); const replies = []; const errors = []; const seen = new Set(); let aliasCount = 0; const count = Math.max(keywords.length, responses.length); for (let index = 0; index < count; index += 1) { const keyword = String(keywords[index] || "").trim(); const aliases = splitConditionalKeywords(keyword); const response = String(responses[index] || "").trim(); if (!keyword && !response) continue; if (!aliases.length || !response) { errors.push("Each conditional reply needs both a keyword and a response."); continue; } for (const alias of aliases) { const normalized = normalizeConditionalKey(alias); if (seen.has(normalized)) errors.push(`The keyword “${alias}” is listed more than once.`); seen.add(normalized); } aliasCount += aliases.length; replies.push({ keyword: aliases.join(", "), response: response.slice(0, 4000) }); } if (!replies.length) errors.push("Add at least one keyword reply."); if (aliasCount > MAX_CONDITIONAL_REPLIES) errors.push(`Use no more than ${MAX_CONDITIONAL_REPLIES} keywords.`); return { ok: errors.length === 0, errors, replies }; } function findConditionalReply(replies, argument, { fuzzy = true } = {}) { const normalizedArgument = normalizeConditionalKey(argument); if (!normalizedArgument) return { match: null, fuzzy: false }; const normalizedReplies = normalizeConditionalReplies(replies); const exact = normalizedReplies.find((entry) => entry.normalized === normalizedArgument); if (exact) return { match: exact, fuzzy: false }; if (!fuzzy) return { match: null, fuzzy: false }; const candidates = normalizedReplies .map((entry) => ({ entry, score: similarity(normalizedArgument, entry.normalized) })) .sort((left, right) => right.score - left.score); const best = candidates[0]; const runnerUp = candidates[1]; const minimum = normalizedArgument.length <= 4 ? 0.82 : 0.68; if (!best || best.score < minimum || (runnerUp && best.score - runnerUp.score < 0.08)) { return { match: null, fuzzy: false }; } return { match: best.entry, fuzzy: true }; } function similarity(left, right) { if (left === right) return 1; const distance = levenshtein(left, right); return 1 - distance / Math.max(left.length, right.length, 1); } function levenshtein(left, right) { const previous = Array.from({ length: right.length + 1 }, (_, index) => index); for (let i = 1; i <= left.length; i += 1) { let diagonal = previous[0]; previous[0] = i; for (let j = 1; j <= right.length; j += 1) { const above = previous[j]; previous[j] = Math.min( previous[j] + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1) ); diagonal = above; } } return previous[right.length]; } function asArray(value) { if (Array.isArray(value)) return value; return value === undefined || value === null ? [] : [value]; } module.exports = { conditionalRepliesFromBody, findConditionalReply, normalizeConditionalKey, normalizeConditionalReplies, normalizeConditionalReplyRows, splitConditionalKeywords };