@mikitasazan/notify 1.24.0 → 1.25.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/dist/routes.d.ts CHANGED
@@ -4,12 +4,12 @@
4
4
  * the projects themselves.
5
5
  *
6
6
  * The scheme: ONE CHAT PER PROJECT. A project with a team is a forum
7
- * supergroup with an "⚙️ Ops" tab (robot notifications) and a "💬 Dev" tab
8
- * (people's live chat). A project the owner runs alone is a plain
9
- * supergroup without topics — the robots post straight into it and there
10
- * is nobody to talk to in a Dev tab (owner's rule of 2026-09-05; the first
11
- * such chat is `htmlg`). A plain chat has no `ops` number: the message
12
- * goes to the chat itself.
7
+ * supergroup with an "Ops" tab (robot notifications, native 🤖 topic icon)
8
+ * and a "Dev" tab (people's live chat, 💬). A project the owner runs alone
9
+ * is a plain supergroup without topics — the robots post straight into it
10
+ * and there is nobody to talk to in a Dev tab (owner's rule of 2026-09-05;
11
+ * the first such chat is `htmlg`). A plain chat has no `ops` number: the
12
+ * message goes to the chat itself.
13
13
  *
14
14
  * Why not one shared forum with a topic per project — that was the first
15
15
  * version, and it turned out to be a mistake: Telegram cannot hide one
@@ -31,10 +31,11 @@ type Forum = {
31
31
  /** The id of the project's supergroup (forum or plain). */
32
32
  chat: string;
33
33
  /**
34
- * The chat's own title, and ONLY for a chat shared by several repositories
35
- * of one product. A repository is a project row here, but the chat belongs
36
- * to the product above them, so its name cannot be derived from any single
37
- * row — every row sharing a chat carries the same `title`. Left out
34
+ * The chat's own title, wherever it cannot be derived from the project key:
35
+ * a product of several repositories (every row sharing that chat carries
36
+ * the same `title`), and a team forum whose product name is prettier than
37
+ * its repository key (owner's rule of 2026-09-10: a FORUM wears the
38
+ * product's own name; a solo chat wears the repository's). Left out
38
39
  * everywhere else, where `chatTitle()` derives the name from the project.
39
40
  */
40
41
  title?: string;
@@ -56,8 +57,9 @@ export declare const ROUTES: Record<Project, Forum>;
56
57
  * A solo project's chat is «<Name> · Ops»; a team forum is just the name — the
57
58
  * forum already carries its own «Ops»/«Dev» tabs, so the chat's own title
58
59
  * does not need to repeat the word. A chat shared by several repositories of
59
- * one product carries the product's name in `title`, and every row sharing
60
- * that chat repeats it, so any of them answers with the same title. The real
60
+ * one product, or a forum whose product name differs from the repo key,
61
+ * carries the product's name in `title`, and every row sharing that chat
62
+ * repeats it, so any of them answers with the same title. The real
61
63
  * Telegram chat title and this function must agree: the avatar scripts and the
62
64
  * name-drift check both read it through `notify routes --json`, not by asking
63
65
  * Telegram.
package/dist/routes.js CHANGED
@@ -31,15 +31,18 @@ export const ROUTES = {
31
31
  // runs it alone.
32
32
  '2roles': { chat: '-1004314188744' },
33
33
  // Created 09.09.2026 by create-ops-chat.py as «Market Lens»: the owner and
34
- // Semyon, so a forum; the tab numbers come from `notify setup`.
35
- 'market-lens': { chat: '-1004489147617', ops: 4, dev: 5 }
34
+ // Semyon, so a forum; the tab numbers come from `notify setup`. A forum
35
+ // wears the product's own name even when it is one repository (owner's
36
+ // rule of 2026-09-10) — hence `title`, like Zabukai above.
37
+ 'market-lens': { chat: '-1004489147617', title: 'Market Lens', ops: 4, dev: 5 }
36
38
  };
37
39
  /**
38
40
  * A solo project's chat is «<Name> · Ops»; a team forum is just the name — the
39
41
  * forum already carries its own «Ops»/«Dev» tabs, so the chat's own title
40
42
  * does not need to repeat the word. A chat shared by several repositories of
41
- * one product carries the product's name in `title`, and every row sharing
42
- * that chat repeats it, so any of them answers with the same title. The real
43
+ * one product, or a forum whose product name differs from the repo key,
44
+ * carries the product's name in `title`, and every row sharing that chat
45
+ * repeats it, so any of them answers with the same title. The real
43
46
  * Telegram chat title and this function must agree: the avatar scripts and the
44
47
  * name-drift check both read it through `notify routes --json`, not by asking
45
48
  * Telegram.
package/dist/setup.js CHANGED
@@ -1,24 +1,42 @@
1
1
  /**
2
- * `notify setup "<Project name>"` creates the "⚙️ Ops" and "💬 Dev" tabs in a
3
- * forum that already exists, and prints the ready line for `ROUTES`.
2
+ * `notify setup <forum chat_id> <project key>` creates the "Ops" and "Dev"
3
+ * tabs in a forum that already exists, and prints the ready line for `ROUTES`.
4
+ *
5
+ * Tab shape (the standard tests/check-telegram-shape.py enforces): plain
6
+ * titles with a NATIVE topic icon, never an emoji in the title text — Ops
7
+ * carries 🤖 (robots post there), Dev carries 💬 (people talk there). The
8
+ * icon ids below are documents of Telegram's topic-icon sticker set,
9
+ * resolved live on 2026-09-10; ⚙️ is NOT in that set, which is why the
10
+ * robot glyph is 🤖.
11
+ *
12
+ * A product of SEVERAL repositories shares one forum, one Ops tab PER
13
+ * repository (a card belongs to the repo it came from) and ONE Dev tab.
14
+ * The Bot API cannot list a forum's topics, so the shape is decided from
15
+ * `ROUTES` itself: peers are rows that already point at this chat.
4
16
  *
5
17
  * The bot cannot create the forum supergroup itself — Telegram only allows a
6
- * real account to do that. So the order for a new project is:
18
+ * real account to do that. So the order for a new project is
19
+ * (mac-config's tg-forums skill, scripts/create-ops-chat.py --forum does all
20
+ * of step 1):
7
21
  * 1. create a group in Telegram, turn on "Topics" in it, add
8
22
  * @mikita_ops_bot as an admin with the "Manage topics" right;
9
- * 2. run `notify setup <chat_id>` — it creates both tabs and prints the line;
23
+ * 2. run `notify setup <chat_id> <project key>` — it creates the tabs and
24
+ * prints the line;
10
25
  * 3. paste the line into `src/routes.ts`.
11
- *
12
- * Step 1 happens once per project and takes half a minute. Steps 2 and 3 are
13
- * mechanical.
14
26
  */
27
+ import { ROUTES } from "./routes.js";
15
28
  const log = (msg) => console.error(`[notify] ${msg}`);
16
- const createTopic = async (token, chat, name, color) => {
29
+ // Documents of Telegram's topic-icon set (InputStickerSetEmojiDefaultTopicIcons):
30
+ // 🤖 for Ops, 💬 for Dev. A wrong id here writes a dead icon into every new
31
+ // forum — tests/check-telegram-shape.py catches a dropped icon live.
32
+ const OPS_ICON_ID = '5309832892262654231'; // 🤖
33
+ const DEV_ICON_ID = '5417915203100613993'; // 💬
34
+ const createTopic = async (token, chat, name, iconId, color) => {
17
35
  try {
18
36
  const res = await fetch(`https://api.telegram.org/bot${token}/createForumTopic`, {
19
37
  method: 'POST',
20
38
  headers: { 'Content-Type': 'application/json' },
21
- body: JSON.stringify({ chat_id: chat, name, icon_color: color }),
39
+ body: JSON.stringify({ chat_id: chat, name, icon_color: color, icon_custom_emoji_id: iconId }),
22
40
  signal: AbortSignal.timeout(10_000)
23
41
  });
24
42
  const body = (await res.json());
@@ -40,21 +58,35 @@ export const setupTopic = async (chatId, projectKey) => {
40
58
  log('no OPS_BOT_TOKEN — cannot create the tabs');
41
59
  return;
42
60
  }
43
- const ops = await createTopic(token, chatId, '⚙️ Ops', 9367192);
44
- const dev = await createTopic(token, chatId, '💬 Dev', 7322096);
61
+ // The Bot API cannot enumerate topics, so the forum's shape is read from
62
+ // ROUTES: a row already pointing here is a peer repository of the same
63
+ // product.
64
+ const existing = ROUTES[projectKey];
65
+ if (existing && existing.chat === chatId && existing.ops !== undefined) {
66
+ log(`${projectKey} is already in ROUTES with ops=${existing.ops} — nothing to create`);
67
+ return;
68
+ }
69
+ const peers = Object.entries(ROUTES).filter(([key, row]) => key !== projectKey && row.chat === chatId);
70
+ // A second repository of the same product gets its name on its own Ops tab;
71
+ // the product's FIRST tab is plain "Ops" (rename it when a second repo
72
+ // joins — tests/check-telegram-shape.py goes red until it is done).
73
+ const opsTitle = peers.length > 0 ? `Ops · ${projectKey}` : 'Ops';
74
+ const sharedDev = peers.map(([, row]) => row.dev).find((id) => id !== undefined);
75
+ const ops = await createTopic(token, chatId, opsTitle, OPS_ICON_ID, 9367192);
76
+ const dev = sharedDev ?? (await createTopic(token, chatId, 'Dev', DEV_ICON_ID, 7322096));
45
77
  // Partial success: if only Ops was created, print it. Otherwise running the
46
78
  // command again would create A DIFFERENT Ops topic, and the old id would be lost.
47
79
  if (ops === null) {
48
80
  log('Ops was not created — check: is the bot a group admin with "Manage topics", and are topics on?');
49
81
  return;
50
82
  }
51
- if (dev === null) {
83
+ if (dev === null || dev === undefined) {
52
84
  log(`Ops created (id=${ops}), Dev was not — add Dev by hand or run again, and keep ops=${ops}`);
53
85
  log('add to src/routes.ts (fill dev in afterwards):');
54
86
  log(` ${JSON.stringify(projectKey)}: { chat: '${chatId}', ops: ${ops}, dev: <fill in> },`);
55
87
  return;
56
88
  }
57
- log(`tabs created: Ops=${ops}, Dev=${dev}`);
89
+ log(`tabs created: Ops="${opsTitle}" (${ops}), Dev (${dev})${sharedDev ? ' [shared with a peer repo]' : ''}`);
58
90
  log('add to src/routes.ts:');
59
91
  log(` ${JSON.stringify(projectKey)}: { chat: '${chatId}', ops: ${ops}, dev: ${dev} },`);
60
92
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikitasazan/notify",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов",
5
5
  "type": "module",
6
6
  "license": "MIT",