@link-assistant/hive-mind 2.13.1 ā 2.13.2
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/buildUserMention.lib.mjs +30 -3
- package/src/github-url-parser.lib.mjs +26 -1
- package/src/session-monitor.lib.mjs +3 -2
- package/src/telegram-accept-invitations.lib.mjs +5 -3
- package/src/telegram-bot.mjs +18 -9
- package/src/telegram-command-execution.lib.mjs +2 -1
- package/src/telegram-context-safety.lib.mjs +70 -0
- package/src/telegram-fix-command.lib.mjs +68 -4
- package/src/telegram-language-command.lib.mjs +4 -3
- package/src/telegram-log-command.lib.mjs +14 -12
- package/src/telegram-markdown-validator.lib.mjs +192 -0
- package/src/telegram-merge-command.lib.mjs +18 -16
- package/src/telegram-message-filters.lib.mjs +1 -1
- package/src/telegram-safe-reply.lib.mjs +290 -21
- package/src/telegram-solve-queue-command.lib.mjs +2 -1
- package/src/telegram-solve-queue.lib.mjs +16 -7
- package/src/telegram-start-stop-command.lib.mjs +38 -27
- package/src/telegram-subscribers.lib.mjs +6 -4
- package/src/telegram-terminal-watch-command.lib.mjs +8 -7
- package/src/telegram-tokens-command.lib.mjs +2 -1
- package/src/telegram-top-command.lib.mjs +8 -9
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Faithful port of TDLib's legacy-Markdown parser used by the Telegram Bot API
|
|
3
|
+
* (`parse_mode: 'Markdown'`), so the bot can tell *before* sending whether
|
|
4
|
+
* Telegram will reject a message with
|
|
5
|
+
* `Bad Request: can't parse entities: Can't find end of the entity starting at byte offset N`.
|
|
6
|
+
*
|
|
7
|
+
* Reference implementation: td/telegram/MessageEntity.cpp ā `parse_markdown()`
|
|
8
|
+
* https://github.com/tdlib/td/blob/master/td/telegram/MessageEntity.cpp
|
|
9
|
+
*
|
|
10
|
+
* The two properties that matter for this repository (issue #2166):
|
|
11
|
+
*
|
|
12
|
+
* 1. Only `\_`, `\*`, `` \` `` and `\[` are recognised as escape sequences, and
|
|
13
|
+
* **only at top level**. Inside an entity (for example the label of a
|
|
14
|
+
* `[label](url)` link) every byte is copied verbatim, so `\_` there is shown
|
|
15
|
+
* to the user as a literal backslash + underscore.
|
|
16
|
+
* 2. An unterminated `_`, `*`, `` ` `` or `[` makes the whole message fail with
|
|
17
|
+
* HTTP 400 and a *byte* offset pointing at the opening character.
|
|
18
|
+
*
|
|
19
|
+
* @module telegram-markdown-validator.lib
|
|
20
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2166
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const UNDERSCORE = 0x5f; // _
|
|
24
|
+
const ASTERISK = 0x2a; // *
|
|
25
|
+
const BACKTICK = 0x60; // `
|
|
26
|
+
const OPEN_BRACKET = 0x5b; // [
|
|
27
|
+
const CLOSE_BRACKET = 0x5d; // ]
|
|
28
|
+
const BACKSLASH = 0x5c; // \
|
|
29
|
+
const OPEN_PAREN = 0x28; // (
|
|
30
|
+
const CLOSE_PAREN = 0x29; // )
|
|
31
|
+
const LF = 0x0a;
|
|
32
|
+
const CR = 0x0d;
|
|
33
|
+
|
|
34
|
+
/** Parse modes this module knows how to validate. */
|
|
35
|
+
export const VALIDATABLE_PARSE_MODES = Object.freeze(['Markdown']);
|
|
36
|
+
|
|
37
|
+
function isUtf8CharacterFirstCodeUnit(byte) {
|
|
38
|
+
return (byte & 0xc0) !== 0x80;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSpaceByte(byte) {
|
|
42
|
+
return byte === 0x20 || byte === 0x09 || byte === LF || byte === 0x0b || byte === 0x0c || byte === CR;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse `text` exactly like TDLib's `parse_markdown()`.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} text - Message text as it would be sent with `parse_mode: 'Markdown'`.
|
|
49
|
+
* @returns {{ok: true, text: string, entities: Array<{type: string, offset: number, length: number, url?: string}>}
|
|
50
|
+
* |{ok: false, byteOffset: number, description: string}}
|
|
51
|
+
* On success, the rendered plain text (escapes removed, markup consumed) plus
|
|
52
|
+
* the entities Telegram would create. On failure, the byte offset of the
|
|
53
|
+
* unterminated entity and the exact Bot API error description.
|
|
54
|
+
*/
|
|
55
|
+
export function parseTelegramLegacyMarkdown(text) {
|
|
56
|
+
const source = Buffer.from(String(text ?? ''), 'utf-8');
|
|
57
|
+
const size = source.length;
|
|
58
|
+
// TDLib relies on std::string's NUL terminator when it peeks past the end.
|
|
59
|
+
const at = index => (index >= 0 && index < size ? source[index] : 0);
|
|
60
|
+
|
|
61
|
+
const out = Buffer.alloc(size);
|
|
62
|
+
let resultSize = 0;
|
|
63
|
+
let utf16Offset = 0;
|
|
64
|
+
const entities = [];
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < size; i++) {
|
|
67
|
+
const c = source[i];
|
|
68
|
+
if (c === BACKSLASH && (at(i + 1) === UNDERSCORE || at(i + 1) === ASTERISK || at(i + 1) === BACKTICK || at(i + 1) === OPEN_BRACKET)) {
|
|
69
|
+
i++;
|
|
70
|
+
out[resultSize++] = source[i];
|
|
71
|
+
utf16Offset++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (c !== UNDERSCORE && c !== ASTERISK && c !== BACKTICK && c !== OPEN_BRACKET) {
|
|
75
|
+
if (isUtf8CharacterFirstCodeUnit(c)) utf16Offset += 1 + (c >= 0xf0 ? 1 : 0);
|
|
76
|
+
out[resultSize++] = source[i];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// We are at the beginning of an entity.
|
|
81
|
+
const beginPos = i;
|
|
82
|
+
let endCharacter = c;
|
|
83
|
+
let isPre = false;
|
|
84
|
+
if (c === OPEN_BRACKET) endCharacter = CLOSE_BRACKET;
|
|
85
|
+
|
|
86
|
+
i++;
|
|
87
|
+
|
|
88
|
+
let language = '';
|
|
89
|
+
if (c === BACKTICK && at(i) === BACKTICK && at(i + 1) === BACKTICK) {
|
|
90
|
+
i += 2;
|
|
91
|
+
isPre = true;
|
|
92
|
+
let languageEnd = i;
|
|
93
|
+
while (!isSpaceByte(at(languageEnd)) && at(languageEnd) !== BACKTICK) languageEnd++;
|
|
94
|
+
if (i !== languageEnd && languageEnd < size && at(languageEnd) !== BACKTICK) {
|
|
95
|
+
language = source.slice(i, languageEnd).toString('utf-8');
|
|
96
|
+
i = languageEnd;
|
|
97
|
+
}
|
|
98
|
+
// Skip one newline at the beginning of the text.
|
|
99
|
+
if (at(i) === LF || at(i) === CR) {
|
|
100
|
+
if ((at(i + 1) === LF || at(i + 1) === CR) && at(i) !== at(i + 1)) i += 2;
|
|
101
|
+
else i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entityOffset = utf16Offset;
|
|
106
|
+
while (i < size && (source[i] !== endCharacter || (isPre && !(at(i + 1) === BACKTICK && at(i + 2) === BACKTICK)))) {
|
|
107
|
+
const cur = source[i];
|
|
108
|
+
if (isUtf8CharacterFirstCodeUnit(cur)) utf16Offset += 1 + (cur >= 0xf0 ? 1 : 0);
|
|
109
|
+
out[resultSize++] = source[i++];
|
|
110
|
+
}
|
|
111
|
+
if (i === size) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
byteOffset: beginPos,
|
|
115
|
+
description: `Bad Request: can't parse entities: Can't find end of the entity starting at byte offset ${beginPos}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (entityOffset !== utf16Offset) {
|
|
120
|
+
const entityLength = utf16Offset - entityOffset;
|
|
121
|
+
if (c === UNDERSCORE) entities.push({ type: 'italic', offset: entityOffset, length: entityLength });
|
|
122
|
+
else if (c === ASTERISK) entities.push({ type: 'bold', offset: entityOffset, length: entityLength });
|
|
123
|
+
else if (c === OPEN_BRACKET) {
|
|
124
|
+
let url;
|
|
125
|
+
if (at(i + 1) !== OPEN_PAREN) {
|
|
126
|
+
url = source.slice(beginPos + 1, i).toString('utf-8');
|
|
127
|
+
} else {
|
|
128
|
+
i += 2;
|
|
129
|
+
const urlStart = i;
|
|
130
|
+
while (i < size && source[i] !== CLOSE_PAREN) i++;
|
|
131
|
+
url = source.slice(urlStart, i).toString('utf-8');
|
|
132
|
+
}
|
|
133
|
+
if (url) entities.push({ type: 'text_link', offset: entityOffset, length: entityLength, url });
|
|
134
|
+
} else if (c === BACKTICK) {
|
|
135
|
+
if (isPre) entities.push({ type: 'pre', offset: entityOffset, length: entityLength, ...(language ? { language } : {}) });
|
|
136
|
+
else entities.push({ type: 'code', offset: entityOffset, length: entityLength });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (isPre) i += 2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { ok: true, text: out.slice(0, resultSize).toString('utf-8'), entities };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Render a small window of `text` around a byte offset, for logs.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} text
|
|
149
|
+
* @param {number} byteOffset
|
|
150
|
+
* @param {number} [radius=32]
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function describeByteOffsetContext(text, byteOffset, radius = 32) {
|
|
154
|
+
const buffer = Buffer.from(String(text ?? ''), 'utf-8');
|
|
155
|
+
const start = Math.max(0, byteOffset - radius);
|
|
156
|
+
const end = Math.min(buffer.length, byteOffset + radius);
|
|
157
|
+
return JSON.stringify(buffer.slice(start, end).toString('utf-8'));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check whether Telegram would accept `text` under `parseMode`.
|
|
162
|
+
*
|
|
163
|
+
* Parse modes other than legacy `Markdown` (and plain text) are reported as
|
|
164
|
+
* valid ā this module deliberately only models the parser the bot uses.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} text
|
|
167
|
+
* @param {string|undefined|null} parseMode
|
|
168
|
+
* @returns {{valid: boolean, description?: string, byteOffset?: number, context?: string}}
|
|
169
|
+
*/
|
|
170
|
+
export function validateTelegramText(text, parseMode) {
|
|
171
|
+
if (!parseMode || String(parseMode) !== 'Markdown') return { valid: true };
|
|
172
|
+
if (typeof text !== 'string' || text.length === 0) return { valid: true };
|
|
173
|
+
const parsed = parseTelegramLegacyMarkdown(text);
|
|
174
|
+
if (parsed.ok) return { valid: true };
|
|
175
|
+
return {
|
|
176
|
+
valid: false,
|
|
177
|
+
description: parsed.description,
|
|
178
|
+
byteOffset: parsed.byteOffset,
|
|
179
|
+
context: describeByteOffsetContext(text, parsed.byteOffset),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Convenience predicate used by tests and by pre-send checks.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} text
|
|
187
|
+
* @param {string} [parseMode='Markdown']
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
export function isValidTelegramMarkdown(text, parseMode = 'Markdown') {
|
|
191
|
+
return validateTelegramText(text, parseMode).valid;
|
|
192
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { checkLabelPermissions, ensureReadyLabel } from './github-merge.lib.mjs';
|
|
22
|
+
import { safeReply, safeEditMessageText } from './telegram-safe-reply.lib.mjs';
|
|
22
23
|
import { extractMergeTargetUrlFromText, parseMergeTargetUrl } from './github-merge-targets.lib.mjs';
|
|
23
24
|
import { createMergeQueueProcessor, MergeStatus, MERGE_QUEUE_CONFIG } from './telegram-merge-queue.lib.mjs';
|
|
24
25
|
import { executeStartScreen } from './telegram-command-execution.lib.mjs';
|
|
@@ -331,12 +332,12 @@ export function registerMergeCommand(bot, options) {
|
|
|
331
332
|
const targetResult = resolveMergeCommandTarget(positionals, ctx.message);
|
|
332
333
|
|
|
333
334
|
if (!targetResult.target && !targetResult.error) {
|
|
334
|
-
return await ctx
|
|
335
|
+
return await safeReply(ctx, getMergeUsageMessage(), { parse_mode: 'MarkdownV2', reply_to_message_id: ctx.message.message_id });
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
if (!targetResult.target || !targetResult.target.valid) {
|
|
338
339
|
const invalidPrefix = targetResult.fromReply ? 'Invalid merge target in replied message' : 'Invalid merge target';
|
|
339
|
-
return await ctx
|
|
340
|
+
return await safeReply(ctx, `${invalidPrefix}: ${escapeMarkdownV2(targetResult.error || targetResult.target?.error || 'Unknown target error')}\n\nPlease provide a valid GitHub repository, issue, or pull request URL\\.`, {
|
|
340
341
|
parse_mode: 'MarkdownV2',
|
|
341
342
|
reply_to_message_id: ctx.message.message_id,
|
|
342
343
|
});
|
|
@@ -352,7 +353,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
352
353
|
if (activeMergeOperations.has(repoKey)) {
|
|
353
354
|
const existingOp = activeMergeOperations.get(repoKey);
|
|
354
355
|
if (existingOp.processor.status === MergeStatus.RUNNING) {
|
|
355
|
-
return await ctx
|
|
356
|
+
return await safeReply(ctx, `A merge operation is already running for ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}\\.\n\nPlease wait for it to complete or cancel it\\.`, {
|
|
356
357
|
parse_mode: 'MarkdownV2',
|
|
357
358
|
reply_to_message_id: ctx.message.message_id,
|
|
358
359
|
});
|
|
@@ -360,7 +361,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
360
361
|
}
|
|
361
362
|
|
|
362
363
|
// Send initial status message (reply to the /merge command)
|
|
363
|
-
const statusMessage = await ctx
|
|
364
|
+
const statusMessage = await safeReply(ctx, `Initializing merge queue for ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}\\.\\.\\.\n\nThis may take a moment\\.`, {
|
|
364
365
|
parse_mode: 'MarkdownV2',
|
|
365
366
|
reply_to_message_id: ctx.message.message_id,
|
|
366
367
|
});
|
|
@@ -369,14 +370,14 @@ export function registerMergeCommand(bot, options) {
|
|
|
369
370
|
// Check permissions
|
|
370
371
|
const permCheck = await checkLabelPermissions(owner, repo, VERBOSE);
|
|
371
372
|
if (!permCheck.canManageLabels) {
|
|
372
|
-
await ctx.telegram
|
|
373
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `No permission to manage repository ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}\\.\n\nPlease ensure you have write access to this repository\\.`, { parse_mode: 'MarkdownV2' });
|
|
373
374
|
return;
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
// Ensure ready label exists
|
|
377
378
|
const labelResult = await ensureReadyLabel(owner, repo, VERBOSE);
|
|
378
379
|
if (!labelResult.success) {
|
|
379
|
-
await ctx.telegram
|
|
380
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `Failed to setup 'ready' label: ${escapeMarkdownV2(labelResult.error)}`, {
|
|
380
381
|
parse_mode: 'MarkdownV2',
|
|
381
382
|
});
|
|
382
383
|
return;
|
|
@@ -401,7 +402,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
401
402
|
// Without this check, progress updates from CI wait loops would re-add
|
|
402
403
|
// the cancel button after the cancel handler had already removed it.
|
|
403
404
|
const replyMarkup = processor.isCancelled ? undefined : { inline_keyboard: [[{ text: 'š Cancel', callback_data: `merge_cancel_${repoKey}` }]] };
|
|
404
|
-
await ctx.telegram
|
|
405
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, message, {
|
|
405
406
|
parse_mode: 'MarkdownV2',
|
|
406
407
|
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
|
407
408
|
});
|
|
@@ -416,7 +417,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
416
417
|
try {
|
|
417
418
|
const message = processor.formatFinalMessage();
|
|
418
419
|
// Remove cancel button on completion
|
|
419
|
-
await ctx.telegram
|
|
420
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, message, {
|
|
420
421
|
parse_mode: 'MarkdownV2',
|
|
421
422
|
});
|
|
422
423
|
VERBOSE && console.log(`[VERBOSE] /merge: Merge queue completed successfully for ${repoKey}`);
|
|
@@ -435,7 +436,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
435
436
|
const finalReport = processor.formatFinalMessage();
|
|
436
437
|
// Issue #1269: Show error in the reply message with immediate feedback
|
|
437
438
|
// Keep a button so users know the error was displayed and can dismiss
|
|
438
|
-
await ctx.telegram
|
|
439
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `ā *Merge queue failed*\n\nā ļø *Error:* ${escapeMarkdownV2(userMessage)}\n\n${finalReport}`, {
|
|
439
440
|
parse_mode: 'MarkdownV2',
|
|
440
441
|
reply_markup: {
|
|
441
442
|
inline_keyboard: [[{ text: 'ā Failed - Click to dismiss', callback_data: `merge_dismiss_${repoKey}` }]],
|
|
@@ -454,7 +455,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
454
455
|
|
|
455
456
|
if (!initResult.success) {
|
|
456
457
|
const userMessage = formatUserError(new Error(initResult.error), VERBOSE);
|
|
457
|
-
await ctx.telegram
|
|
458
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `Failed to initialize merge queue: ${escapeMarkdownV2(userMessage)}`, {
|
|
458
459
|
parse_mode: 'MarkdownV2',
|
|
459
460
|
});
|
|
460
461
|
return;
|
|
@@ -462,14 +463,14 @@ export function registerMergeCommand(bot, options) {
|
|
|
462
463
|
|
|
463
464
|
if (initResult.message) {
|
|
464
465
|
// No PRs to merge
|
|
465
|
-
await ctx.telegram
|
|
466
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `*Merge Queue \\- ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}*${labelMsg}\n\n${escapeMarkdownV2(initResult.message)}\n\nTo use the merge queue:\n1\\. Add the \`ready\` label to repository PRs, or ensure the issue has a linked open PR\n2\\. Run \`/merge ${escapeMarkdownV2(targetUrl)}\` again`, { parse_mode: 'MarkdownV2' });
|
|
466
467
|
return;
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
// Update message with PR list and cancel button, start processing
|
|
470
471
|
const truncatedMsg = initResult.truncated ? `\n\n_Note: Only processing first ${MERGE_QUEUE_CONFIG.MAX_PRS_PER_SESSION} PRs_` : '';
|
|
471
472
|
|
|
472
|
-
await ctx.telegram
|
|
473
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `*Merge Queue \\- ${escapeMarkdownV2(owner)}/${escapeMarkdownV2(repo)}*${labelMsg}\n\n${getTargetFoundText(target, initResult.count)}${escapeMarkdownV2(truncatedMsg)}\n\nStarting merge process\\.\\.\\.`, {
|
|
473
474
|
parse_mode: 'MarkdownV2',
|
|
474
475
|
reply_markup: {
|
|
475
476
|
inline_keyboard: [[{ text: 'š Cancel', callback_data: `merge_cancel_${repoKey}` }]],
|
|
@@ -501,7 +502,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
501
502
|
// Show error in the reply message with immediate feedback
|
|
502
503
|
try {
|
|
503
504
|
const userMessage = formatUserError(error, VERBOSE);
|
|
504
|
-
await ctx.telegram
|
|
505
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `ā *Merge queue failed unexpectedly*\n\nā ļø *Error:* ${escapeMarkdownV2(userMessage)}\n\n_The queue processing has stopped\\. Please try again or check server logs\\._`, {
|
|
505
506
|
parse_mode: 'MarkdownV2',
|
|
506
507
|
reply_markup: {
|
|
507
508
|
inline_keyboard: [[{ text: 'ā Failed - Click to dismiss', callback_data: `merge_dismiss_${repoKey}` }]],
|
|
@@ -519,7 +520,7 @@ export function registerMergeCommand(bot, options) {
|
|
|
519
520
|
|
|
520
521
|
try {
|
|
521
522
|
const userMessage = formatUserError(error, VERBOSE);
|
|
522
|
-
await ctx.telegram
|
|
523
|
+
await safeEditMessageText(ctx.telegram, statusMessage.chat.id, statusMessage.message_id, undefined, `Error processing merge queue: ${escapeMarkdownV2(userMessage)}\n\nPlease check the repository URL and try again\\.`, { parse_mode: 'MarkdownV2' });
|
|
523
524
|
} catch (editError) {
|
|
524
525
|
VERBOSE && console.error('[VERBOSE] /merge: Failed to edit error message:', editError);
|
|
525
526
|
}
|
|
@@ -547,9 +548,10 @@ export function registerMergeCommand(bot, options) {
|
|
|
547
548
|
// the current PR finishes processing (which can take hours if waiting for CI).
|
|
548
549
|
try {
|
|
549
550
|
const cancellingMessage = operation.processor.formatProgressMessage();
|
|
550
|
-
|
|
551
|
+
// No reply_markup = cancel button is removed immediately
|
|
552
|
+
await safeEditMessageText(ctx.telegram, ctx.chat?.id, ctx.callbackQuery?.message?.message_id, undefined, cancellingMessage, {
|
|
551
553
|
parse_mode: 'MarkdownV2',
|
|
552
|
-
|
|
554
|
+
verbose: VERBOSE,
|
|
553
555
|
});
|
|
554
556
|
} catch (err) {
|
|
555
557
|
// If the full message edit fails, fall back to just removing the button
|
|
@@ -243,7 +243,7 @@ export function extractGitHubUrl(text, { parseGitHubUrl, cleanNonPrintableChars
|
|
|
243
243
|
|
|
244
244
|
// Accept issue or PR URLs
|
|
245
245
|
if (parsed.valid && (parsed.type === 'issue' || parsed.type === 'pull')) {
|
|
246
|
-
foundUrls.push(parsed.normalized);
|
|
246
|
+
foundUrls.push(parsed.canonical || parsed.normalized);
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|