fix: telegram allowFrom matching and entity resolution

Fixes three critical bugs in Telegram integration:

1. Case-insensitive username matching
   - Telegram usernames are case-insensitive, but the API returns them with original casing
   - Now lowercase all usernames when extracting sender identifier to match config entries
   - Fixes: messages from @ArneMoor now match config entry telegram:@arnemoor

2. Entity resolution for sending replies
   - resolveEntity() now strips telegram: prefix before resolving users/chats
   - Fixes: "Could not resolve Telegram entity: telegram:@username" errors when sending replies

3. Improved skip logging
   - Messages skipped due to allowFrom filtering now always logged (not just in verbose mode)
   - Uses clear emoji indicator: ⏭️ Skipped message from...
This commit is contained in:
Arne Moor 2025-12-06 10:19:19 +01:00
parent 7290734f2b
commit 31246f0a34
3 changed files with 17 additions and 9 deletions

View File

@ -60,13 +60,14 @@ export async function convertTelegramMessage(
* Extract sender identifier with telegram: prefix to prevent session ID collisions.
*
* Returns:
* - telegram:@username (if username available)
* - telegram:@username (if username available, lowercase for case-insensitive matching)
* - telegram:+phone (if phone available, normalized to E.164)
* - telegram:id (numeric Telegram ID as fallback)
*/
function extractSenderIdentifier(sender: Api.User | Api.Chat): string {
if ("username" in sender && sender.username) {
return `telegram:@${sender.username}`;
// Lowercase username for case-insensitive matching (Telegram usernames are case-insensitive)
return `telegram:@${sender.username.toLowerCase()}`;
}
if ("phone" in sender && sender.phone) {
// Ensure phone has + prefix for E.164 format to match normalized allowFrom entries
@ -209,6 +210,10 @@ export function isAllowedSender(
return true; // No whitelist = allow all
}
if (allowFrom.includes("*")) {
return true; // Wildcard allows all senders
}
const normalizedFrom = normalizeAllowFromEntry(message.from, "telegram");
const normalizedAllowList = allowFrom.map((e) =>
normalizeAllowFromEntry(e, "telegram"),

View File

@ -170,11 +170,10 @@ async function handleInboundMessage(
normalizeAllowFromEntry(e, "telegram"),
);
if (!normalizedAllowList.includes(normalizedFrom)) {
if (isVerbose()) {
logVerbose(
`Skipping auto-reply: sender ${message.from} not in allowFrom list`,
);
}
// Always log skipped messages (not just in verbose mode)
runtime.log(
`⏭️ Skipped message from ${message.from} (not in allowFrom list)`,
);
return;
}
}

View File

@ -6,13 +6,17 @@ type Entity = Api.User | Api.Chat | Api.Channel;
/**
* Resolve Telegram entity (user/chat) from identifier.
* Supports @username, phone number, or user ID.
* Also handles telegram: prefix (e.g., telegram:@username).
*/
export async function resolveEntity(
client: TelegramClient,
identifier: string,
): Promise<Entity> {
// Clean identifier
const clean = identifier.trim();
// Clean identifier and strip telegram: prefix if present
let clean = identifier.trim();
if (clean.startsWith("telegram:")) {
clean = clean.slice("telegram:".length);
}
// Try as-is first (handles @username, phone, user ID)
try {