mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
Hide WhatsApp LID ids from session-header peer labels (4.9.1-ops.21).
Resolve nickname/phone via contact LID mapping and show only group title, pushName, and phone. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
37742043a9
commit
a92080e490
5 changed files with 331 additions and 215 deletions
|
|
@ -571,7 +571,7 @@ var React25 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "dsh-im-ops",
|
name: "dsh-im-ops",
|
||||||
version: "4.9.1-ops.20",
|
version: "4.9.1-ops.21",
|
||||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 \u2014 access/session policies owned here.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
376
lib/index.js
376
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-im-ops",
|
"name": "dsh-im-ops",
|
||||||
"version": "4.9.1-ops.20",
|
"version": "4.9.1-ops.21",
|
||||||
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 — access/session policies owned here.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,56 @@ export function parseConversationKey(key) {
|
||||||
return conversationId ? { kind: 'group', conversationId, senderId: null } : null;
|
return conversationId ? { kind: 'group', conversationId, senderId: null } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function contactPushName(grant, phone) {
|
/**
|
||||||
if (!phone || !grant || !Array.isArray(grant.contacts)) return '';
|
* Normalize a WhatsApp LID identity for contact matching.
|
||||||
const hit = grant.contacts.find((contact) => contact?.phone === phone);
|
* Accepts `digits@lid`, `digits:device@lid`, or a bare lid token already stored on contacts.
|
||||||
return typeof hit?.pushName === 'string' ? hit.pushName.trim() : '';
|
* @param {unknown} value
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
export function lidFromWhatsappIdentity(value) {
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
const raw = value.trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
const at = raw.indexOf('@');
|
||||||
|
if (at > 0) {
|
||||||
|
const server = raw.slice(at + 1).toLowerCase();
|
||||||
|
if (server !== 'lid' && server !== 'hosted.lid') return null;
|
||||||
|
const user = raw.slice(0, at).split(':')[0].trim();
|
||||||
|
return user ? `${user}@lid` : null;
|
||||||
|
}
|
||||||
|
// Contacts may already store the canonical `digits@lid` form only.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lidsMatch(stored, needle) {
|
||||||
|
if (typeof stored !== 'string' || !needle) return false;
|
||||||
|
const left = lidFromWhatsappIdentity(stored) ?? stored.trim();
|
||||||
|
return left === needle || left === needle.replace(/@lid$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve contact phone/pushName from grant by PN or LID.
|
||||||
|
* @param {object|null} grant
|
||||||
|
* @param {string|null} identity
|
||||||
|
* @returns {{ phone: string|null, pushName: string|null }}
|
||||||
|
*/
|
||||||
|
export function resolveContactIdentity(grant, identity) {
|
||||||
|
const phone = phoneFromWhatsappJid(identity) ?? normalizeAccessPhone(identity);
|
||||||
|
const lid = lidFromWhatsappIdentity(identity);
|
||||||
|
const contacts = Array.isArray(grant?.contacts) ? grant.contacts : [];
|
||||||
|
const hit = contacts.find((contact) => {
|
||||||
|
if (phone && contact?.phone === phone) return true;
|
||||||
|
if (lid && Array.isArray(contact?.lids) && contact.lids.some((entry) => lidsMatch(entry, lid))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}) ?? null;
|
||||||
|
const resolvedPhone = phone
|
||||||
|
?? (typeof hit?.phone === 'string' && hit.phone ? hit.phone : null);
|
||||||
|
const pushName = typeof hit?.pushName === 'string' && hit.pushName.trim()
|
||||||
|
? hit.pushName.trim()
|
||||||
|
: null;
|
||||||
|
return { phone: resolvedPhone, pushName };
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupTitle(grant, groupJid) {
|
function groupTitle(grant, groupJid) {
|
||||||
|
|
@ -36,12 +82,21 @@ function groupTitle(grant, groupJid) {
|
||||||
return typeof title === 'string' && title.trim() ? title.trim() : '';
|
return typeof title === 'string' && title.trim() ? title.trim() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function phoneFromIdentity(value) {
|
/**
|
||||||
return phoneFromWhatsappJid(value) ?? normalizeAccessPhone(value);
|
* Join human-facing label parts. Never includes raw JIDs / LIDs.
|
||||||
|
* @param {...(string|null|undefined)} parts
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatChannelPeerLabel(...parts) {
|
||||||
|
return parts
|
||||||
|
.map((part) => (typeof part === 'string' ? part.trim() : ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' · ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a public channel-peer summary for session-header display.
|
* Build a public channel-peer summary for session-header display.
|
||||||
|
* Label shows only group title / nickname / phone — never opaque WhatsApp ids.
|
||||||
* @param {{
|
* @param {{
|
||||||
* channel: string,
|
* channel: string,
|
||||||
* botId: string,
|
* botId: string,
|
||||||
|
|
@ -61,12 +116,9 @@ export function resolveChannelPeerFromBinding(input) {
|
||||||
const grant = input.grant && typeof input.grant === 'object' ? input.grant : null;
|
const grant = input.grant && typeof input.grant === 'object' ? input.grant : null;
|
||||||
|
|
||||||
if (parsed.kind === 'direct') {
|
if (parsed.kind === 'direct') {
|
||||||
const phone = phoneFromIdentity(parsed.conversationId);
|
const { phone, pushName } = resolveContactIdentity(grant, parsed.conversationId);
|
||||||
const pushName = contactPushName(grant, phone);
|
const label = formatChannelPeerLabel(pushName, phone);
|
||||||
const who = pushName || phone || parsed.conversationId;
|
if (!label) return null;
|
||||||
const label = phone && pushName && pushName !== phone
|
|
||||||
? `${pushName} · ${phone}`
|
|
||||||
: who;
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
channel,
|
channel,
|
||||||
botId,
|
botId,
|
||||||
|
|
@ -74,8 +126,8 @@ export function resolveChannelPeerFromBinding(input) {
|
||||||
conversationKey,
|
conversationKey,
|
||||||
conversationId: parsed.conversationId,
|
conversationId: parsed.conversationId,
|
||||||
senderId: null,
|
senderId: null,
|
||||||
phone: phone || null,
|
phone,
|
||||||
pushName: pushName || null,
|
pushName,
|
||||||
groupTitle: null,
|
groupTitle: null,
|
||||||
label,
|
label,
|
||||||
});
|
});
|
||||||
|
|
@ -93,13 +145,12 @@ export function resolveChannelPeerFromBinding(input) {
|
||||||
phone: null,
|
phone: null,
|
||||||
pushName: null,
|
pushName: null,
|
||||||
groupTitle: title,
|
groupTitle: title,
|
||||||
label: `群 · ${title}`,
|
label: title,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const phone = phoneFromIdentity(parsed.senderId);
|
const { phone, pushName } = resolveContactIdentity(grant, parsed.senderId);
|
||||||
const pushName = contactPushName(grant, phone);
|
const label = formatChannelPeerLabel(title, pushName, phone) || title;
|
||||||
const who = pushName || phone || parsed.senderId;
|
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
channel,
|
channel,
|
||||||
botId,
|
botId,
|
||||||
|
|
@ -107,9 +158,9 @@ export function resolveChannelPeerFromBinding(input) {
|
||||||
conversationKey,
|
conversationKey,
|
||||||
conversationId: parsed.conversationId,
|
conversationId: parsed.conversationId,
|
||||||
senderId: parsed.senderId,
|
senderId: parsed.senderId,
|
||||||
phone: phone || null,
|
phone,
|
||||||
pushName: pushName || null,
|
pushName,
|
||||||
groupTitle: title,
|
groupTitle: title,
|
||||||
label: `${title} · ${who}`,
|
label,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ import { join } from 'node:path';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
formatChannelPeerLabel,
|
||||||
|
lidFromWhatsappIdentity,
|
||||||
parseConversationKey,
|
parseConversationKey,
|
||||||
resolveChannelPeerFromBinding,
|
resolveChannelPeerFromBinding,
|
||||||
|
resolveContactIdentity,
|
||||||
} from '../../../src/channels/shared/channel-peer.mjs';
|
} from '../../../src/channels/shared/channel-peer.mjs';
|
||||||
import { ConversationStateStore } from '../../../src/channels/shared/conversation-state-store.mjs';
|
import { ConversationStateStore } from '../../../src/channels/shared/conversation-state-store.mjs';
|
||||||
|
|
||||||
|
|
@ -32,7 +35,11 @@ test('parseConversationKey covers direct, shared group, and per-user group', ()
|
||||||
|
|
||||||
test('resolveChannelPeerFromBinding builds DM and group labels from access grant', () => {
|
test('resolveChannelPeerFromBinding builds DM and group labels from access grant', () => {
|
||||||
const grant = {
|
const grant = {
|
||||||
contacts: [{ phone: '8618222222222', pushName: 'Alice' }],
|
contacts: [{
|
||||||
|
phone: '8618222222222',
|
||||||
|
lids: ['91010910658657@lid'],
|
||||||
|
pushName: 'Alice',
|
||||||
|
}],
|
||||||
groups: {
|
groups: {
|
||||||
'120363@g.us': { title: 'Ops Room', admins: [], members: [] },
|
'120363@g.us': { title: 'Ops Room', admins: [], members: [] },
|
||||||
},
|
},
|
||||||
|
|
@ -47,14 +54,25 @@ test('resolveChannelPeerFromBinding builds DM and group labels from access grant
|
||||||
assert.equal(direct.kind, 'direct');
|
assert.equal(direct.kind, 'direct');
|
||||||
assert.equal(direct.phone, '8618222222222');
|
assert.equal(direct.phone, '8618222222222');
|
||||||
|
|
||||||
|
const lidDirect = resolveChannelPeerFromBinding({
|
||||||
|
channel: 'whatsapp',
|
||||||
|
botId: 'bot-1',
|
||||||
|
conversationKey: 'direct:91010910658657@lid',
|
||||||
|
grant,
|
||||||
|
});
|
||||||
|
assert.equal(lidDirect.label, 'Alice · 8618222222222');
|
||||||
|
assert.equal(lidDirect.phone, '8618222222222');
|
||||||
|
assert.doesNotMatch(lidDirect.label, /@lid/);
|
||||||
|
|
||||||
const groupUser = resolveChannelPeerFromBinding({
|
const groupUser = resolveChannelPeerFromBinding({
|
||||||
channel: 'whatsapp',
|
channel: 'whatsapp',
|
||||||
botId: 'bot-1',
|
botId: 'bot-1',
|
||||||
conversationKey: 'group:120363@g.us:user:8618222222222@s.whatsapp.net',
|
conversationKey: 'group:120363@g.us:user:91010910658657@lid',
|
||||||
grant,
|
grant,
|
||||||
});
|
});
|
||||||
assert.equal(groupUser.label, 'Ops Room · Alice');
|
assert.equal(groupUser.label, 'Ops Room · Alice · 8618222222222');
|
||||||
assert.equal(groupUser.groupTitle, 'Ops Room');
|
assert.equal(groupUser.groupTitle, 'Ops Room');
|
||||||
|
assert.doesNotMatch(groupUser.label, /@lid/);
|
||||||
|
|
||||||
const groupShared = resolveChannelPeerFromBinding({
|
const groupShared = resolveChannelPeerFromBinding({
|
||||||
channel: 'whatsapp',
|
channel: 'whatsapp',
|
||||||
|
|
@ -62,7 +80,54 @@ test('resolveChannelPeerFromBinding builds DM and group labels from access grant
|
||||||
conversationKey: 'group:120363@g.us',
|
conversationKey: 'group:120363@g.us',
|
||||||
grant,
|
grant,
|
||||||
});
|
});
|
||||||
assert.equal(groupShared.label, '群 · Ops Room');
|
assert.equal(groupShared.label, 'Ops Room');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveChannelPeerFromBinding never falls back to raw LID when contact is incomplete', () => {
|
||||||
|
const grant = {
|
||||||
|
contacts: [{ lids: ['91010910658657@lid'], pushName: 'Bob' }],
|
||||||
|
groups: {
|
||||||
|
'120363@g.us': { title: 'test2', admins: [], members: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const direct = resolveChannelPeerFromBinding({
|
||||||
|
channel: 'whatsapp',
|
||||||
|
botId: 'bot-1',
|
||||||
|
conversationKey: 'direct:91010910658657@lid',
|
||||||
|
grant,
|
||||||
|
});
|
||||||
|
assert.equal(direct.label, 'Bob');
|
||||||
|
assert.equal(direct.phone, null);
|
||||||
|
|
||||||
|
const unknown = resolveChannelPeerFromBinding({
|
||||||
|
channel: 'whatsapp',
|
||||||
|
botId: 'bot-1',
|
||||||
|
conversationKey: 'direct:111222333444@lid',
|
||||||
|
grant,
|
||||||
|
});
|
||||||
|
assert.equal(unknown, null);
|
||||||
|
|
||||||
|
const groupOnlyLid = resolveChannelPeerFromBinding({
|
||||||
|
channel: 'whatsapp',
|
||||||
|
botId: 'bot-1',
|
||||||
|
conversationKey: 'group:120363@g.us:user:111222333444@lid',
|
||||||
|
grant,
|
||||||
|
});
|
||||||
|
assert.equal(groupOnlyLid.label, 'test2');
|
||||||
|
assert.doesNotMatch(groupOnlyLid.label, /@lid|\d{10,}@/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lid and label helpers stay human-facing', () => {
|
||||||
|
assert.equal(lidFromWhatsappIdentity('91010910658657@lid'), '91010910658657@lid');
|
||||||
|
assert.equal(lidFromWhatsappIdentity('91010910658657:12@lid'), '91010910658657@lid');
|
||||||
|
assert.equal(lidFromWhatsappIdentity('8618222222222@s.whatsapp.net'), null);
|
||||||
|
assert.equal(formatChannelPeerLabel('Ops', '', '8618', null), 'Ops · 8618');
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveContactIdentity({
|
||||||
|
contacts: [{ phone: '8618', lids: ['9@lid'], pushName: 'N' }],
|
||||||
|
}, '9@lid'),
|
||||||
|
{ phone: '8618', pushName: 'N' },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ConversationStateStore.keyForSession reverses session bindings', async () => {
|
test('ConversationStateStore.keyForSession reverses session bindings', async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue