mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
135 lines
4.3 KiB
JavaScript
135 lines
4.3 KiB
JavaScript
const CHANNELS = new Set([
|
|
'weixin',
|
|
'feishu',
|
|
'dingtalk',
|
|
'wecom',
|
|
'qq',
|
|
'slack',
|
|
'telegram',
|
|
'discord',
|
|
'whatsapp',
|
|
]);
|
|
|
|
function isRecord(value) {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function opaqueId(value) {
|
|
return typeof value === 'string'
|
|
&& value.length > 0
|
|
&& value.length <= 512
|
|
&& value.trim() === value
|
|
&& !/[\s:\u0000-\u001f\u007f]/u.test(value);
|
|
}
|
|
|
|
function afterPrefix(key, prefix) {
|
|
const marker = `${prefix}:`;
|
|
if (!key.startsWith(marker)) return null;
|
|
const value = key.slice(marker.length);
|
|
return opaqueId(value) ? value : null;
|
|
}
|
|
|
|
function simpleSuggestion(key, definitions) {
|
|
for (const [prefix, kind, field] of definitions) {
|
|
const value = afterPrefix(key, prefix);
|
|
if (value) return { kind, route: { [field]: value } };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function feishuSuggestion(key) {
|
|
const openId = afterPrefix(key, 'p2p');
|
|
if (openId?.startsWith('ou_')) {
|
|
return { kind: 'user', route: { openId } };
|
|
}
|
|
return simpleSuggestion(key, [['group', 'group', 'chatId']]);
|
|
}
|
|
|
|
function slackSuggestion(key) {
|
|
const directChannel = afterPrefix(key, 'direct');
|
|
if (directChannel && /^[A-Za-z0-9_-]{1,128}$/.test(directChannel)) {
|
|
return { kind: 'conversation', route: { channelId: directChannel } };
|
|
}
|
|
const match = /^group:([^:]+):(\d{1,20}(?:\.\d{1,20})?)$/.exec(key);
|
|
if (!match || !/^[A-Za-z0-9_-]{1,128}$/.test(match[1])) return null;
|
|
return { kind: 'thread', route: { channelId: match[1], threadTs: match[2] } };
|
|
}
|
|
|
|
function telegramSuggestion(key) {
|
|
const match = /^(direct|group):(-?\d+)(?::([1-9]\d*))?$/.exec(key);
|
|
if (!match) return null;
|
|
const chatId = Number(match[2]);
|
|
if (!Number.isSafeInteger(chatId)) return null;
|
|
if (match[1] === 'direct' && match[3] !== undefined) return null;
|
|
if (match[3] === undefined) return { kind: 'chat', route: { chatId: match[2] } };
|
|
const messageThreadId = Number(match[3]);
|
|
if (!Number.isSafeInteger(messageThreadId) || messageThreadId <= 0) return null;
|
|
return { kind: 'topic', route: { chatId: match[2], messageThreadId } };
|
|
}
|
|
|
|
function discordSuggestion(key) {
|
|
const match = /^(?:direct|group):(\d{1,32})$/.exec(key);
|
|
return match ? { kind: 'channel', route: { channelId: match[1] } } : null;
|
|
}
|
|
|
|
function whatsappSuggestion(key) {
|
|
const direct = /^direct:(\d{5,32}@(s\.whatsapp\.net|lid))$/.exec(key);
|
|
if (direct) return { kind: 'user', route: { jid: direct[1] } };
|
|
const group = /^group:(\d{5,32}(?:-\d{1,32})?@g\.us)$/.exec(key);
|
|
return group ? { kind: 'group', route: { jid: group[1] } } : null;
|
|
}
|
|
|
|
/** Convert one persisted conversation key into a stable proactive-delivery route. */
|
|
export function deliverySuggestionFromConversationKey(channel, key) {
|
|
if (!CHANNELS.has(channel) || typeof key !== 'string') return null;
|
|
switch (channel) {
|
|
case 'weixin':
|
|
return simpleSuggestion(key, [['p2p', 'user', 'toUserId']]);
|
|
case 'feishu':
|
|
return feishuSuggestion(key);
|
|
case 'dingtalk':
|
|
return simpleSuggestion(key, [
|
|
['p2p', 'user', 'userId'],
|
|
['group', 'group', 'openConversationId'],
|
|
]);
|
|
case 'wecom':
|
|
return simpleSuggestion(key, [
|
|
['direct', 'user', 'chatId'],
|
|
['group', 'group', 'chatId'],
|
|
]);
|
|
case 'qq':
|
|
return simpleSuggestion(key, [
|
|
['c2c', 'user', 'userOpenId'],
|
|
['group', 'group', 'groupOpenId'],
|
|
]);
|
|
case 'slack':
|
|
return slackSuggestion(key);
|
|
case 'telegram':
|
|
return telegramSuggestion(key);
|
|
case 'discord':
|
|
return discordSuggestion(key);
|
|
case 'whatsapp':
|
|
return whatsappSuggestion(key);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract and de-duplicate stable delivery routes from a persisted sessions map.
|
|
* Session ids and every other state field are intentionally ignored.
|
|
*/
|
|
export function deliverySuggestionsFromSessions(channel, sessions) {
|
|
if (!CHANNELS.has(channel) || !isRecord(sessions)) return [];
|
|
const suggestions = [];
|
|
const seen = new Set();
|
|
for (const key of Object.keys(sessions)) {
|
|
const suggestion = deliverySuggestionFromConversationKey(channel, key);
|
|
if (!suggestion) continue;
|
|
const identity = JSON.stringify(suggestion);
|
|
if (seen.has(identity)) continue;
|
|
seen.add(identity);
|
|
suggestions.push(suggestion);
|
|
}
|
|
return suggestions;
|
|
}
|