Skip to content

feat: notifications support Telegram topics in supergroups#493

Merged
jotka merged 1 commit intoFinsys:mainfrom
yokkkoso:feat/tg-topic-id
Apr 8, 2026
Merged

feat: notifications support Telegram topics in supergroups#493
jotka merged 1 commit intoFinsys:mainfrom
yokkkoso:feat/tg-topic-id

Conversation

@yokkkoso
Copy link
Copy Markdown
Contributor

@yokkkoso yokkkoso commented Feb 7, 2026

Proposed change

Add support for sending notifications to Topics in Telegram supergroups, as Apprise supports it

Type of change

  • Bug fix: non-breaking change which fixes an issue.
  • New feature / Enhancement: non-breaking change which adds functionality.
  • Breaking change: fix or feature that would cause existing functionality to not work as expected.
  • Other. Please explain:

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Feb 7, 2026

CLA assistant check
All committers have signed the CLA.

@jotka jotka merged commit 14967bd into Finsys:main Apr 8, 2026
1 check passed
@Chinoman10
Copy link
Copy Markdown

I think this PR may not fully fix the original issue because it only supports the new Dockhand-specific syntax:

  • tgram://bot_token/chat_id:topic_id

but the documented Apprise Telegram syntax is:

  • tgram://{bot_token}/{chat_id}:{topic}/

Notice the trailing slash. Apprise documents this as valid syntax, and also documents related forms like:

  • tgram://{bot_token}/{chat_id}/

  • tgram://{bot_token}/{chat_id}:{topic}/

  • tgram://{bot_token}/{chat_id1}:topic1}/{chat_id2}:{topic2}/{chat_id3}:{topic3}/

Apprise Telegram syntax:
https://github.com/caronc/apprise/wiki/Notify_telegram

Telegram Bot API field:
message_thread_id

The current regex added by this PR is:

/^tgram:\/\/([^/]+)\/([^:\/]+)(?::(\d+))?$/

That matches:

tgram://bot_token/chat_id
tgram://bot_token/chat_id:topic_id

But does not match the Apprise-documented forms with a trailing slash:

tgram://bot_token/chat_id/
tgram://bot_token/chat_id:topic_id/

So if users copy the syntax from Apprise docs, the topic may still not be parsed/sent as intended.

A possible minimal fix would be to accept the inline :topic_id form with or without a trailing slash:

// tgram://bot_token/chat_id
// tgram://bot_token/chat_id/
// tgram://bot_token/chat_id:topic_id
// tgram://bot_token/chat_id:topic_id/
const match = appriseUrl.match(
  /^tgram:\/\/([^/]+)\/([^:/?]+)(?::(\d+))?\/?$/
);

if (!match) {
  return {
    success: false,
    error:
      'Invalid Telegram URL format. Expected: tgram://bot_token/chat_id or tgram://bot_token/chat_id:topic_id/'
  };
}

const [, botToken, chatId, topicIdStr] = match;
const topicId = topicIdStr ? Number.parseInt(topicIdStr, 10) : undefined;

const url = `https://api.telegram.org/bot${botToken}/sendMessage`;

const escapedTitle = escapeTelegramMarkdown(payload.title);
const escapedMessage = escapeTelegramMarkdown(payload.message);
const envTag = payload.environmentName
  ? ` \\[${escapeTelegramMarkdown(payload.environmentName)}\\]`
  : '';

const response = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chat_id: chatId,
    text: `*${escapedTitle}*${envTag}\n${escapedMessage}`,
    ...(Number.isFinite(topicId) ? { message_thread_id: topicId } : {}),
    parse_mode: 'Markdown'
  })
});

If Dockhand wants to be a bit closer to Apprise compatibility, it could also support ?topic= explicitly:

// tgram://bot_token/chat_id
// tgram://bot_token/chat_id/
// tgram://bot_token/chat_id:topic_id
// tgram://bot_token/chat_id:topic_id/
// tgram://bot_token/chat_id/?topic=topic_id
const match = appriseUrl.match(
  /^tgram:\/\/([^/]+)\/([^:/?]+)(?::(\d+))?\/?(?:\?(.+))?$/
);

if (!match) {
  return {
    success: false,
    error:
      'Invalid Telegram URL format. Expected: tgram://bot_token/chat_id, tgram://bot_token/chat_id:topic_id/, or tgram://bot_token/chat_id/?topic=topic_id'
  };
}

const [, botToken, chatId, inlineTopicId, queryString] = match;

const queryTopicId = queryString
  ? new URLSearchParams(queryString).get('topic')
  : null;

const topicIdStr = inlineTopicId ?? queryTopicId ?? undefined;
const topicId = topicIdStr ? Number.parseInt(topicIdStr, 10) : undefined;

const url = `https://api.telegram.org/bot${botToken}/sendMessage`;

const escapedTitle = escapeTelegramMarkdown(payload.title);
const escapedMessage = escapeTelegramMarkdown(payload.message);
const envTag = payload.environmentName
  ? ` \\[${escapeTelegramMarkdown(payload.environmentName)}\\]`
  : '';

const response = await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    chat_id: chatId,
    text: `*${escapedTitle}*${envTag}\n${escapedMessage}`,
    ...(Number.isFinite(topicId) ? { message_thread_id: topicId } : {}),
    parse_mode: 'Markdown'
  })
});

This still does not implement the full Apprise Telegram URL grammar, especially multiple chat IDs in one URL, but it should cover the exact syntax used in the original bug report and avoid silently excluding the documented trailing-slash form.

/cc @jotka

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants