Files
memind/wechat/push-subscription-parser.mjs
T

208 lines
7.1 KiB
JavaScript
Raw Normal View History

import { parseHourMinute } from '../schedule-intent.mjs';
import { normalizePushWeatherCity } from './push-content/weather-delivery.mjs';
import {
getPushSubscriptionById,
PUSH_SUBSCRIPTION_ITEMS,
resolvePushSubscriptionFromAlias,
} from './push-subscription-catalog.mjs';
const SUBSCRIPTION_ID_PATTERN = /^([1-9]|10)$/u;
const SUBSCRIPTION_ID_PREFIX_PATTERN = /^([1-9]|10)[\s::,,、-]+(.+)$/u;
const SUBSCRIPTION_MODIFY_PATTERN = /^([1-9]|10)\s*(改|换|调整|设为|设置为|调到|改到|换成|改为)\s*(.+)$/u;
const SUBSCRIPTION_CANCEL_ID_PATTERN = /^取消\s*([1-9]|10)$/u;
const LIST_REQUEST_PATTERN = /^(我的订阅|订阅状态|订阅列表|已开通订阅)$/u;
const HELP_REQUEST_PATTERN = /^(订阅帮助|订阅菜单|推送菜单|推送帮助)$/u;
const CANCEL_ALL_PATTERN = /^(全部取消|取消全部|取消所有订阅)$/u;
const WEATHER_CITY_UPDATE_PATTERN = /^([1-9]|10)\s*(改|换|调整|设为|设置为|调到|改到|换成|改为)\s*城市\s*(.+)$/u;
const WEATHER_ALIAS_CITY_UPDATE_PATTERN = /^(天气|每日天气)\s*(改|换|调整|设为|设置为|调到|改到|换成|改为)\s*城市?\s*(.+)$/u;
function parseCityToken(raw) {
const text = String(raw ?? '')
.trim()
.replace(/^(城市|设为|设置为|改成|改为|换为|换成)/u, '')
.replace(/\s+/g, '')
.replace(/市$/u, '');
if (!text || text.length > 6) return null;
if (!/^[\u4e00-\u9fff]{2,6}$/u.test(text)) return null;
return normalizePushWeatherCity(text);
}
function parseWeatherConfirmSegment(segment) {
const raw = String(segment ?? '').trim();
if (!raw) return { hour: null, minute: null, options: null };
const time = parseHourMinute(raw);
if (time) {
const remainder = raw
.replace(/(?:今天|明天|后天|今晚|明晚|明早|今早)?(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?(?:[0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|:)(?:半|[0-9]{1,2}分?)?/u, '')
.trim();
const city = parseCityToken(remainder);
return {
hour: time.hour,
minute: time.minute,
options: city ? { city } : null,
};
}
const city = parseCityToken(raw);
if (city) {
return { hour: null, minute: null, options: { city } };
}
return { hour: null, minute: null, options: null };
}
function parseSubscriptionIdToken(token) {
const raw = String(token ?? '').trim();
if (SUBSCRIPTION_ID_PATTERN.test(raw)) return Number(raw);
if (raw === '1') return 1;
if (raw === '2') return 2;
if (raw === '3') return 3;
if (raw === '4') return 4;
if (raw === '5') return 5;
if (raw === '6') return 6;
if (raw === '7') return 7;
if (raw === '8') return 8;
if (raw === '9') return 9;
if (raw === '10') return 10;
return null;
}
/**
* 解析开通指令:`2` / `2 7点` / `2 7:30`
* @returns {{ item: object, hour: number|null, minute: number|null }|null}
*/
export function parsePushSubscriptionConfirmReply(
text,
{ defaultHourResolver = null } = {},
) {
const raw = String(text ?? '').trim();
if (!raw) return null;
const compact = raw.replace(/\s+/g, '');
const bareId = parseSubscriptionIdToken(compact);
if (bareId != null && compact.length <= 2) {
const item = getPushSubscriptionById(bareId);
if (!item) return null;
return { item, hour: null, minute: null };
}
const prefixed = raw.match(SUBSCRIPTION_ID_PREFIX_PATTERN);
if (prefixed) {
const item = getPushSubscriptionById(Number(prefixed[1]));
if (!item) return null;
if (item.key === 'weather') {
const weather = parseWeatherConfirmSegment(prefixed[2]);
return {
item,
hour: weather.hour,
minute: weather.minute,
options: weather.options,
};
}
const time = parseHourMinute(prefixed[2]);
return { item, hour: time?.hour ?? null, minute: time?.minute ?? null, options: null };
}
return null;
}
/** @returns {{ item: object, options: { city: string } }|null} */
export function parsePushSubscriptionCityUpdate(text) {
const raw = String(text ?? '').trim();
if (!raw) return null;
const byId = raw.match(WEATHER_CITY_UPDATE_PATTERN);
if (byId) {
const item = getPushSubscriptionById(Number(byId[1]));
const city = parseCityToken(byId[3]);
if (!item || item.key !== 'weather' || !city) return null;
return { item, options: { city } };
}
const byAlias = raw.replace(/\s+/g, '').match(WEATHER_ALIAS_CITY_UPDATE_PATTERN);
if (byAlias) {
const item = resolvePushSubscriptionFromAlias('天气');
const city = parseCityToken(byAlias[3]);
if (!item || !city) return null;
return { item, options: { city } };
}
return null;
}
/** @returns {{ item: object }|{ all: true }|null} */
export function parsePushSubscriptionCancelRequest(text) {
const raw = String(text ?? '').trim();
const compact = raw.replace(/\s+/g, '');
if (!compact) return null;
if (CANCEL_ALL_PATTERN.test(compact)) return { all: true };
const byId = compact.match(SUBSCRIPTION_CANCEL_ID_PATTERN);
if (byId) {
const item = getPushSubscriptionById(Number(byId[1]));
return item ? { item } : null;
}
const aliasItem = resolvePushSubscriptionFromAlias(compact);
if (!aliasItem) return null;
const hasCancelCue = /(取消|关闭|不要|停止)/u.test(compact);
if (!hasCancelCue) return null;
return { item: aliasItem };
}
/** @returns {{ item: object, hour: number, minute: number }|null} */
export function parsePushSubscriptionModifyRequest(text) {
const raw = String(text ?? '').trim();
if (!raw) return null;
const byId = raw.match(SUBSCRIPTION_MODIFY_PATTERN);
if (byId) {
const item = getPushSubscriptionById(Number(byId[1]));
const time = parseHourMinute(byId[3]);
if (!item || !time) return null;
return { item, hour: time.hour, minute: time.minute };
}
const compact = raw.replace(/\s+/g, '');
const aliasItem = resolvePushSubscriptionFromAlias(compact);
const hasModifyCue = /(改|换|调整|设为|设置为|调到|改到|换成|改为)/u.test(compact);
if (!aliasItem || !hasModifyCue) return null;
const time = parseHourMinute(raw);
if (!time) return null;
return { item: aliasItem, hour: time.hour, minute: time.minute };
}
export function parsePushSubscriptionListRequest(text) {
const compact = String(text ?? '').replace(/\s+/g, '');
return LIST_REQUEST_PATTERN.test(compact);
}
export function parsePushSubscriptionHelpRequest(text) {
const compact = String(text ?? '').replace(/\s+/g, '');
return HELP_REQUEST_PATTERN.test(compact);
}
export function resolvePushSubscriptionConfirmTime(
item,
{ hour = null, minute = null, defaultHourResolver = null } = {},
) {
if (Number.isInteger(hour) && hour >= 0 && hour <= 23
&& Number.isInteger(minute) && minute >= 0 && minute <= 59) {
return { hour, minute };
}
if (typeof defaultHourResolver === 'function') {
const resolved = defaultHourResolver(item);
if (resolved) return resolved;
}
return { hour: item.defaultHour, minute: item.defaultMinute };
}
export function isPushSubscriptionNumericReply(text) {
return parsePushSubscriptionConfirmReply(text) != null;
}
export function listPushSubscriptionAliasKeys() {
return PUSH_SUBSCRIPTION_ITEMS.flatMap((item) => item.aliases);
}