@xmanrui/dsh-im 4.38.2 → 4.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +4 -2
- package/lib/index.js +311 -306
- package/package.json +1 -1
- package/plugin-src/host/injected-context.mjs +13 -1
- package/src/channels/matrix/matrix-api.mjs +34 -0
- package/src/channels/matrix/matrix-config-store.mjs +50 -2
- package/src/channels/matrix/matrix-normalize.mjs +106 -14
- package/src/channels/matrix/matrix-room-history.mjs +240 -8
- package/src/channels/matrix/matrix-runtime.mjs +563 -3
- package/src/channels/shared/context-enhancement.mjs +2 -0
- package/src/channels/shared/conversation/ambient-provider.mjs +115 -0
- package/src/channels/shared/conversation/context-builder.mjs +148 -0
- package/src/channels/shared/conversation/reply-context.mjs +95 -0
- package/src/channels/shared/conversation/root-context.mjs +101 -0
- package/src/channels/shared/conversation/route.mjs +71 -0
- package/src/channels/shared/conversation/semantic-message.mjs +75 -0
- package/src/channels/shared/conversation/trigger-policy.mjs +80 -0
- package/src/channels/shared/conversation-state-store.mjs +1 -1
- package/src/channels/shared/harness-approval.mjs +19 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -0
- package/src/channels/shared/injected-context.mjs +64 -2
- package/src/channels/shared/speaker-label.mjs +46 -0
- package/src/channels/shared/text-harness-bridge.mjs +348 -32
- package/src/channels/wecom/wecom-cards.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.39.0",
|
|
4
4
|
"icon": "./assets/logo-plugin-message-link-gradient.webp",
|
|
5
5
|
"description": "把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.",
|
|
6
6
|
"keywords": [
|
|
@@ -12,6 +12,9 @@ const REPLY_LABELS = Object.freeze({ zh: '\u5f15\u7528', en: 'Quoted' });
|
|
|
12
12
|
/** Row label for a source block that names no readable field of its own. */
|
|
13
13
|
const SOURCE_LABELS = Object.freeze({ zh: '\u6765\u6e90', en: 'Source' });
|
|
14
14
|
|
|
15
|
+
/** Row label prefix for one collected conversation-context block. */
|
|
16
|
+
const CONTEXT_LABELS = Object.freeze({ zh: '\u4e0a\u4e0b\u4e0b', en: 'Context' });
|
|
17
|
+
|
|
15
18
|
/** Read the current reply-row label; an unknown language falls back to English. */
|
|
16
19
|
function replyLabel() {
|
|
17
20
|
try {
|
|
@@ -30,6 +33,15 @@ function sourceLabel() {
|
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/** Read the current context-row label; unknown languages fall back. */
|
|
37
|
+
function contextLabel() {
|
|
38
|
+
try {
|
|
39
|
+
return getImHostLanguage() === 'en' ? CONTEXT_LABELS.en : CONTEXT_LABELS.zh;
|
|
40
|
+
} catch {
|
|
41
|
+
return CONTEXT_LABELS.en;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
/**
|
|
34
46
|
* Pair every injected context block with the user message that carried it, and
|
|
35
47
|
* materialize the source guidance as session-level prompt context.
|
|
@@ -85,7 +97,7 @@ export function installInjectedContext(ctx, { logger, registry = imSourceGuidanc
|
|
|
85
97
|
}
|
|
86
98
|
try {
|
|
87
99
|
const rewritten = rewriteInjectedContextMessages(decision.messages, {
|
|
88
|
-
labels: { reply: replyLabel(), source: sourceLabel() },
|
|
100
|
+
labels: { reply: replyLabel(), source: sourceLabel(), context: contextLabel() },
|
|
89
101
|
ownedGuidance: registry.get(agent?.session?.id),
|
|
90
102
|
});
|
|
91
103
|
return rewritten === null ? decision : { ...decision, messages: rewritten };
|
|
@@ -250,6 +250,24 @@ export class MatrixApi {
|
|
|
250
250
|
return await this.#request('whoami', { path: '/account/whoami', signal: options.signal });
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Reads one member's display name from the room state. The Matrix profile
|
|
255
|
+
* field is optional, so a homeserver without the member or without a name
|
|
256
|
+
* reads as null rather than an error.
|
|
257
|
+
* @param roomId - the room whose state to read.
|
|
258
|
+
* @param userId - the member's fully-qualified Matrix user id.
|
|
259
|
+
* @param options - `{ signal }` forwarding options.
|
|
260
|
+
* @returns {Promise<string|null>} the trimmed display name, or null when absent.
|
|
261
|
+
*/
|
|
262
|
+
async getRoomMemberDisplayName(roomId, userId, options = {}) {
|
|
263
|
+
const event = await this.#request(`room member ${userId}`, {
|
|
264
|
+
path: `/rooms/${encodeURIComponent(roomId)}/state/members/${encodeURIComponent(userId)}`,
|
|
265
|
+
signal: options.signal,
|
|
266
|
+
}).catch(() => null);
|
|
267
|
+
const name = cleanString(event?.content?.displayname);
|
|
268
|
+
return name && name.length <= 256 ? name : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
253
271
|
async login({ identifier, password, deviceId, displayName }, options = {}) {
|
|
254
272
|
const body = {
|
|
255
273
|
type: 'm.login.password',
|
|
@@ -336,6 +354,22 @@ export class MatrixApi {
|
|
|
336
354
|
});
|
|
337
355
|
}
|
|
338
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Single-event lookup used by the conversation context collectors to resolve a
|
|
359
|
+
* reply or thread root outside the retained sync window. The homeserver answers
|
|
360
|
+
* 404 for an unknown event, so a miss travels as null instead of an exception;
|
|
361
|
+
* an E2EE room returns the still-encrypted event and the caller decrypts it.
|
|
362
|
+
*/
|
|
363
|
+
async getEvent(roomId, eventId, options = {}) {
|
|
364
|
+
if (!isMatrixRoomId(roomId) || !isMatrixEventId(eventId)) throw new TypeError('Matrix event lookup target is invalid');
|
|
365
|
+
return await this.#request('event', {
|
|
366
|
+
path: `/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`,
|
|
367
|
+
timeoutMs: 10_000,
|
|
368
|
+
signal: options.signal,
|
|
369
|
+
}).catch(() => null);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
339
373
|
async listRelations(roomId, eventId, relationType, options = {}) {
|
|
340
374
|
if (!isMatrixRoomId(roomId) || !isMatrixEventId(eventId)) throw new TypeError('Matrix relation query target is invalid');
|
|
341
375
|
if (!/^[m]?[A-Za-z0-9._-]{1,128}$/.test(String(relationType ?? ''))) throw new TypeError('Matrix relation type is invalid');
|
|
@@ -16,6 +16,18 @@ const MAX_MEDIA_BYTES_CEILING = 104_857_600;
|
|
|
16
16
|
const ROOM_CONTEXT_LIMIT_BOUNDS = Object.freeze({ min: 1, max: 500 });
|
|
17
17
|
const ROOM_CONTEXT_CHARS_BOUNDS = Object.freeze({ min: 200, max: 200_000 });
|
|
18
18
|
const ROOM_CONTEXT_TZ_BOUNDS = Object.freeze({ min: -840, max: 840 });
|
|
19
|
+
const CONTEXT_CHARS_BOUNDS = Object.freeze({ min: 200, max: 200_000 });
|
|
20
|
+
const REPLY_DEPTH_BOUNDS = Object.freeze({ min: 1, max: 10 });
|
|
21
|
+
const AMBIENT_GAP_BOUNDS = Object.freeze({ min: 5_000, max: 3_600_000 });
|
|
22
|
+
const AMBIENT_WINDOW_BOUNDS = Object.freeze({ min: 10_000, max: 86_400_000 });
|
|
23
|
+
const AMBIENT_COUNT_BOUNDS = Object.freeze({ min: 1, max: 200 });
|
|
24
|
+
const VISIBLE_CONTEXT_WINDOW_BOUNDS = Object.freeze({ min: 0, max: 86_400_000 });
|
|
25
|
+
const VISIBLE_CONTEXT_LIMIT_BOUNDS = Object.freeze({ min: 0, max: 500 });
|
|
26
|
+
const VISIBLE_CONTEXT_CHARS_BOUNDS = Object.freeze({ min: 200, max: 200_000 });
|
|
27
|
+
const COALESCE_WINDOW_BOUNDS = Object.freeze({ min: 0, max: 60_000 });
|
|
28
|
+
const AUTO_STEER_LIMIT_BOUNDS = Object.freeze({ min: 0, max: 100 });
|
|
29
|
+
const THREADS_MODES = Object.freeze(['inherit', 'isolate', 'auto']);
|
|
30
|
+
const TRIGGER_ON_REPLY_MODES = Object.freeze(['any', 'bot-only', 'never']);
|
|
19
31
|
|
|
20
32
|
function cleanString(value) {
|
|
21
33
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
@@ -185,12 +197,31 @@ export class MatrixConfigStore {
|
|
|
185
197
|
const roomContextLimit = boundedInteger(value.roomContextLimit, ROOM_CONTEXT_LIMIT_BOUNDS);
|
|
186
198
|
const roomContextMaxChars = boundedInteger(value.roomContextMaxChars, ROOM_CONTEXT_CHARS_BOUNDS);
|
|
187
199
|
const roomContextTzOffsetMinutes = boundedInteger(value.roomContextTzOffsetMinutes, ROOM_CONTEXT_TZ_BOUNDS);
|
|
200
|
+
const threadsMode = cleanString(value.threadsMode) ?? 'inherit';
|
|
201
|
+
const triggerOnReply = cleanString(value.triggerOnReply) ?? 'never';
|
|
202
|
+
const contextMaxChars = boundedInteger(value.contextMaxChars, CONTEXT_CHARS_BOUNDS);
|
|
203
|
+
const replyMaxDepth = boundedInteger(value.replyMaxDepth, REPLY_DEPTH_BOUNDS);
|
|
204
|
+
const ambientBurstGapMs = boundedInteger(value.ambientBurstGapMs, AMBIENT_GAP_BOUNDS);
|
|
205
|
+
const ambientRecentWindowMs = boundedInteger(value.ambientRecentWindowMs, AMBIENT_WINDOW_BOUNDS);
|
|
206
|
+
const ambientRecentCount = boundedInteger(value.ambientRecentCount, AMBIENT_COUNT_BOUNDS);
|
|
207
|
+
const visibleContextWindowMs = boundedInteger(value.visibleContextWindowMs, VISIBLE_CONTEXT_WINDOW_BOUNDS);
|
|
208
|
+
const visibleContextLimit = boundedInteger(value.visibleContextLimit, VISIBLE_CONTEXT_LIMIT_BOUNDS);
|
|
209
|
+
const visibleContextChars = boundedInteger(value.visibleContextChars, VISIBLE_CONTEXT_CHARS_BOUNDS);
|
|
210
|
+
const coalesceWindowMs = boundedInteger(value.coalesceWindowMs, COALESCE_WINDOW_BOUNDS);
|
|
211
|
+
const autoSteerLimit = boundedInteger(value.autoSteerLimit, AUTO_STEER_LIMIT_BOUNDS);
|
|
188
212
|
if (!E2EE_MODES.has(e2eeMode) || !AUTO_JOIN_MODES.has(autoJoinInvites)
|
|
189
213
|
|| maxMessageLength === null || maxMediaBytes === null
|
|
190
214
|
|| allowedRooms === null || freeResponseRooms === null || ignoreUserPatterns === null
|
|
191
215
|
|| roomContextLimit === null || roomContextMaxChars === null
|
|
192
|
-
|| roomContextTzOffsetMinutes === null
|
|
193
|
-
|
|
216
|
+
|| roomContextTzOffsetMinutes === null
|
|
217
|
+
|| !THREADS_MODES.includes(threadsMode) || !TRIGGER_ON_REPLY_MODES.includes(triggerOnReply)
|
|
218
|
+
|| contextMaxChars === null || replyMaxDepth === null
|
|
219
|
+
|| ambientBurstGapMs === null || ambientRecentWindowMs === null || ambientRecentCount === null
|
|
220
|
+
|| visibleContextWindowMs === null || visibleContextLimit === null || visibleContextChars === null
|
|
221
|
+
|| coalesceWindowMs === null || autoSteerLimit === null) return null;
|
|
222
|
+
for (const key of ['requireMention', 'processNotices', 'allowRoomMentions', 'reactions', 'roomContextEnabled',
|
|
223
|
+
'rootContextOnceEnabled', 'replyContextEnabled', 'ambientContextEnabled',
|
|
224
|
+
'visibleContextEnabled', 'autoSteerSameSender']) {
|
|
194
225
|
if (value?.[key] !== undefined && typeof value[key] !== 'boolean') return null;
|
|
195
226
|
}
|
|
196
227
|
return Object.freeze({
|
|
@@ -220,6 +251,23 @@ export class MatrixConfigStore {
|
|
|
220
251
|
...(roomContextLimit !== undefined ? { roomContextLimit } : {}),
|
|
221
252
|
...(roomContextMaxChars !== undefined ? { roomContextMaxChars } : {}),
|
|
222
253
|
...(roomContextTzOffsetMinutes !== undefined ? { roomContextTzOffsetMinutes } : {}),
|
|
254
|
+
...(threadsMode !== 'inherit' ? { threadsMode } : {}),
|
|
255
|
+
...(triggerOnReply !== 'never' ? { triggerOnReply } : {}),
|
|
256
|
+
...(typeof value.rootContextOnceEnabled === 'boolean' ? { rootContextOnceEnabled: value.rootContextOnceEnabled } : {}),
|
|
257
|
+
...(typeof value.replyContextEnabled === 'boolean' ? { replyContextEnabled: value.replyContextEnabled } : {}),
|
|
258
|
+
...(typeof value.ambientContextEnabled === 'boolean' ? { ambientContextEnabled: value.ambientContextEnabled } : {}),
|
|
259
|
+
...(contextMaxChars !== undefined ? { contextMaxChars } : {}),
|
|
260
|
+
...(replyMaxDepth !== undefined ? { replyMaxDepth } : {}),
|
|
261
|
+
...(ambientBurstGapMs !== undefined ? { ambientBurstGapMs } : {}),
|
|
262
|
+
...(ambientRecentWindowMs !== undefined ? { ambientRecentWindowMs } : {}),
|
|
263
|
+
...(ambientRecentCount !== undefined ? { ambientRecentCount } : {}),
|
|
264
|
+
...(typeof value.visibleContextEnabled === 'boolean' ? { visibleContextEnabled: value.visibleContextEnabled } : {}),
|
|
265
|
+
...(visibleContextWindowMs !== undefined ? { visibleContextWindowMs } : {}),
|
|
266
|
+
...(visibleContextLimit !== undefined ? { visibleContextLimit } : {}),
|
|
267
|
+
...(visibleContextChars !== undefined ? { visibleContextChars } : {}),
|
|
268
|
+
...(coalesceWindowMs !== undefined ? { coalesceWindowMs } : {}),
|
|
269
|
+
...(typeof value.autoSteerSameSender === 'boolean' ? { autoSteerSameSender: value.autoSteerSameSender } : {}),
|
|
270
|
+
...(autoSteerLimit !== undefined ? { autoSteerLimit } : {}),
|
|
223
271
|
});
|
|
224
272
|
}
|
|
225
273
|
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { isMatrixEventId, isMatrixRoomId, isMatrixUserId } from './matrix-api.mjs';
|
|
10
|
+
import { buildConversationRoute, encodeConversationRouteKey } from '../shared/conversation/route.mjs';
|
|
11
|
+
import { evaluateTrigger } from '../shared/conversation/trigger-policy.mjs';
|
|
10
12
|
|
|
11
13
|
const GRACE_MS = 5_000;
|
|
12
14
|
const SKEW_OBSERVE_AFTER_MS = 30_000;
|
|
@@ -219,7 +221,8 @@ function eventOriginTs(event) {
|
|
|
219
221
|
return Number.isSafeInteger(ts) && ts > 0 ? (ts < 1e12 ? ts * 1_000 : ts) : null;
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
/** The thread root a timeline event belongs to, from its MSC3402 relations. */
|
|
225
|
+
export function threadIdOf(content) {
|
|
223
226
|
const relates = content?.['m.relates_to'];
|
|
224
227
|
if (!relates || typeof relates !== 'object') return null;
|
|
225
228
|
const chain = Array.isArray(relates.chain) ? relates.chain : [];
|
|
@@ -228,12 +231,39 @@ function threadIdOf(content) {
|
|
|
228
231
|
return eventId && isMatrixEventId(eventId) ? eventId : null;
|
|
229
232
|
}
|
|
230
233
|
|
|
231
|
-
function
|
|
232
|
-
const
|
|
233
|
-
const eventId = typeof inReplyTo?.event_id === 'string' ? inReplyTo.event_id : null;
|
|
234
|
+
function cleanEventId(value) {
|
|
235
|
+
const eventId = typeof value === 'string' ? value.trim() : '';
|
|
234
236
|
return eventId && isMatrixEventId(eventId) ? eventId : null;
|
|
235
237
|
}
|
|
236
238
|
|
|
239
|
+
/**
|
|
240
|
+
* The target of a genuine reply relation, in either relation form the Matrix
|
|
241
|
+
* spec permits: the new form names it under `m.relates_to` with
|
|
242
|
+
* `rel_type: "m.reply"`, the legacy form under the top-level `m.in_reply_to`,
|
|
243
|
+
* and this plugin's own writer nests it under `m.relates_to` for the events it
|
|
244
|
+
* sends itself. The `is_falling_back` marker — at `m.relates_to`, at the
|
|
245
|
+
* legacy `m.in_reply_to`, or on the nested form — demotes the declaration to
|
|
246
|
+
* the thread-membership statement clients attach automatically when sending
|
|
247
|
+
* inside a thread; per the spec that is not a human reply, so it must not
|
|
248
|
+
* drive reply context collection, reply classification, recorded reply targets,
|
|
249
|
+
* or the thread fork decision.
|
|
250
|
+
*/
|
|
251
|
+
export function replyEventIdOf(content) {
|
|
252
|
+
const relates = content?.['m.relates_to'];
|
|
253
|
+
const legacy = content?.['m.in_reply_to'];
|
|
254
|
+
const nested = relates?.['m.in_reply_to'];
|
|
255
|
+
if (relates?.is_falling_back === true || legacy?.is_falling_back === true || nested?.is_falling_back === true) return null;
|
|
256
|
+
if (relates?.rel_type === 'm.reply') {
|
|
257
|
+
const target = cleanEventId(relates.event_id);
|
|
258
|
+
if (target) return target;
|
|
259
|
+
}
|
|
260
|
+
for (const candidate of [legacy?.event_id, nested?.event_id]) {
|
|
261
|
+
const target = cleanEventId(candidate);
|
|
262
|
+
if (target) return target;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
237
267
|
/**
|
|
238
268
|
* Project one timeline event into the normalized bridge message, or into a
|
|
239
269
|
* typed drop reason. The order of gates is fixed: self, bridge/system,
|
|
@@ -281,10 +311,28 @@ export function normalizeMatrixTimelineEvent({
|
|
|
281
311
|
const rawText = typeof content.body === 'string' ? content.body : '';
|
|
282
312
|
const mentioned = detectMatrixMention(content, botUserId);
|
|
283
313
|
const commandLike = isMatrixCommandLike(rawText);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
314
|
+
// One table decides the mention gate and the addressed bit, shared with other
|
|
315
|
+
// channels through the trigger policy. `triggerOnReply` defaults to 'never' —
|
|
316
|
+
// a room message wakes the bot only through an @-mention or a command, so an
|
|
317
|
+
// unaddressed reply on the room timeline or in a thread stays a recorded fact.
|
|
318
|
+
// 'any' restores the 4.25-era shape where every reply relation addresses the
|
|
319
|
+
// bot; 'bot-only' requires the channel's classifier (usually the bot-event
|
|
320
|
+
// binding table) to prove the reply answers one of the bot's own messages.
|
|
321
|
+
const replyTargetKind = kind === 'group' && replyToEventId !== null
|
|
322
|
+
&& typeof deps.classifyReplyTarget === 'function'
|
|
323
|
+
? deps.classifyReplyTarget(replyToEventId) ?? null
|
|
324
|
+
: null;
|
|
325
|
+
const gate = evaluateTrigger({
|
|
326
|
+
kind,
|
|
327
|
+
requireMention: config.requireMention,
|
|
328
|
+
freeResponse: config.freeResponseRooms instanceof Set && config.freeResponseRooms.has(roomId),
|
|
329
|
+
mentioned,
|
|
330
|
+
commandLike,
|
|
331
|
+
replyToPresent: replyToEventId !== null,
|
|
332
|
+
replyTargetKind,
|
|
333
|
+
replyMode: config.triggerOnReply,
|
|
334
|
+
});
|
|
335
|
+
if (!gate.pass) return { drop: gate.reason };
|
|
288
336
|
|
|
289
337
|
const text = stripMatrixMentions(rawText, botUserId);
|
|
290
338
|
const media = MEDIA_MSGTYPES.has(msgtype)
|
|
@@ -293,9 +341,42 @@ export function normalizeMatrixTimelineEvent({
|
|
|
293
341
|
if (MEDIA_MSGTYPES.has(msgtype) && !media) return { drop: 'media-unusable' };
|
|
294
342
|
if (msgtype !== 'm.text' && msgtype !== 'm.notice' && !MEDIA_MSGTYPES.has(msgtype)) return { drop: 'type' };
|
|
295
343
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
344
|
+
// Thread conversation fork policy. A genuine reply whose thread root equals
|
|
345
|
+
// its reply target says "answer this message" — the client started a thread at
|
|
346
|
+
// the replied-to event — so it inherits the conversation its target belongs to
|
|
347
|
+
// (the room's main conversation) instead of forking a fresh session. The
|
|
348
|
+
// thread-membership declaration clients attach automatically (a falling-back
|
|
349
|
+
// relation) is not a human reply and never selects a conversation: posts made
|
|
350
|
+
// inside a thread fork that thread's own conversation. A reply to a root the
|
|
351
|
+
// bot itself authored does the same — the panel under a bot answer is read as
|
|
352
|
+
// its own message list, not as a continuation of the mainline turn the answer
|
|
353
|
+
// once replied to. `isolate` restores fork-always; `auto` prefers the
|
|
354
|
+
// bot-event binding table when the channel supplies one for the reply target.
|
|
355
|
+
const threadsMode = config.threadsMode === 'isolate' || config.threadsMode === 'auto'
|
|
356
|
+
? config.threadsMode
|
|
357
|
+
: 'inherit';
|
|
358
|
+
const rootIsBotAuthored = kind === 'group' && threadId !== null
|
|
359
|
+
&& ((typeof deps.classifyReplyTarget === 'function' && deps.classifyReplyTarget(threadId) === 'bot')
|
|
360
|
+
|| (typeof deps.isBotRoomEvent === 'function' && deps.isBotRoomEvent(roomId, threadId) === true));
|
|
361
|
+
const inheritsThreadTarget = kind === 'group' && threadId !== null
|
|
362
|
+
&& threadsMode !== 'isolate' && replyToEventId === threadId && !rootIsBotAuthored;
|
|
363
|
+
const routeThreadId = kind === 'group' && threadId !== null && !inheritsThreadTarget ? threadId : null;
|
|
364
|
+
const boundRouteKey = threadsMode === 'auto' && kind === 'group' && replyToEventId !== null
|
|
365
|
+
&& typeof deps.routeOverrideForReply === 'function'
|
|
366
|
+
? deps.routeOverrideForReply(replyToEventId)
|
|
367
|
+
: null;
|
|
368
|
+
const routeKey = typeof boundRouteKey === 'string' && boundRouteKey
|
|
369
|
+
? boundRouteKey
|
|
370
|
+
: encodeConversationRouteKey(buildConversationRoute({
|
|
371
|
+
channel: 'matrix',
|
|
372
|
+
botId: config.botId ?? null,
|
|
373
|
+
chatId: roomId,
|
|
374
|
+
senderId,
|
|
375
|
+
threadId: routeThreadId,
|
|
376
|
+
kind,
|
|
377
|
+
}));
|
|
378
|
+
const keyPrefix = kind === 'direct' ? 'direct:' : 'group:';
|
|
379
|
+
const conversationId = routeKey.startsWith(keyPrefix) ? routeKey.slice(keyPrefix.length) : routeKey;
|
|
299
380
|
|
|
300
381
|
return {
|
|
301
382
|
message: {
|
|
@@ -310,7 +391,7 @@ export function normalizeMatrixTimelineEvent({
|
|
|
310
391
|
plainText: msgtype === 'm.text' && content.format !== 'org.matrix.custom.html',
|
|
311
392
|
images: media?.images ?? [],
|
|
312
393
|
files: media?.files ?? [],
|
|
313
|
-
addressed:
|
|
394
|
+
addressed: gate.addressed,
|
|
314
395
|
mentioned,
|
|
315
396
|
threadId,
|
|
316
397
|
replyToEventId,
|
|
@@ -352,6 +433,17 @@ export function normalizeMatrixDeliveryTarget({ kind, route } = {}) {
|
|
|
352
433
|
|
|
353
434
|
export function matrixConversationKeyFromTarget(target) {
|
|
354
435
|
if (!target) return null;
|
|
355
|
-
if (target.kind === 'dm')
|
|
356
|
-
|
|
436
|
+
if (target.kind === 'dm') {
|
|
437
|
+
return encodeConversationRouteKey(buildConversationRoute({
|
|
438
|
+
channel: 'matrix',
|
|
439
|
+
chatId: target.userId,
|
|
440
|
+
senderId: target.userId,
|
|
441
|
+
kind: 'dm',
|
|
442
|
+
})).slice('direct:'.length);
|
|
443
|
+
}
|
|
444
|
+
return encodeConversationRouteKey(buildConversationRoute({
|
|
445
|
+
channel: 'matrix',
|
|
446
|
+
chatId: target.roomId,
|
|
447
|
+
threadId: 'threadId' in target ? target.threadId : null,
|
|
448
|
+
})).slice('group:'.length);
|
|
357
449
|
}
|