@mikitasazan/notify 1.24.0 → 1.26.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/render.js +13 -3
- package/dist/routes.d.ts +14 -12
- package/dist/routes.js +7 -4
- package/dist/setup.js +45 -13
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -21,14 +21,24 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { DISPLAY, ICON, iconFor } from "./events.js";
|
|
23
23
|
/**
|
|
24
|
-
* A label gets a capital letter — but NOT a name
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* A label gets a capital letter — but NOT a name. Two shapes of a name,
|
|
25
|
+
* both verbatim:
|
|
26
|
+
*
|
|
27
|
+
* 1. An inner-cap spelling a capital would flatten: `iOS` was turning into
|
|
28
|
+
* `IOS`. The signal is a capital second letter.
|
|
29
|
+
* 2. `<name> #N` — a tracked thing named by number. In the tasks digest the
|
|
30
|
+
* name is the repository's own (`zabukai-app #33`), and the one-name rule
|
|
31
|
+
* keeps a repository's name symbol for symbol; this printed
|
|
32
|
+
* «Zabukai-app #33» (owner, 12.09.2026). The ` #` is structural, not a
|
|
33
|
+
* spelling guess — prose labels never carry one.
|
|
27
34
|
*/
|
|
28
35
|
const cap = (s) => {
|
|
29
36
|
if (s.length === 0 || /^[a-z][A-Z]/.test(s)) {
|
|
30
37
|
return s;
|
|
31
38
|
}
|
|
39
|
+
if (/ #[#\w]/.test(s)) {
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
32
42
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
33
43
|
};
|
|
34
44
|
/** Escapes EVERYTHING that comes from outside — only the template adds tags. */
|
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 "
|
|
8
|
-
* (people's live chat). A project the owner runs alone
|
|
9
|
-
* supergroup without topics — the robots post straight into it
|
|
10
|
-
* is nobody to talk to in a Dev tab (owner's rule of 2026-09-05;
|
|
11
|
-
* such chat is `htmlg`). A plain chat has no `ops` number: the
|
|
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,
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
60
|
-
*
|
|
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
|
-
|
|
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
|
|
42
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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