@thibautrey/chatons-channel-telegram 1.0.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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @thibautrey/chatons-channel-telegram
2
+
3
+ Telegram channel extension for Chatons.
4
+
5
+ ## Files
6
+
7
+ - `chaton.extension.json`: extension manifest
8
+ - `index.html`: main webview entry
9
+ - `index.js`: Telegram channel UI and host integration logic
10
+ - `components.js`: shared UI helpers and styles
11
+
12
+ ## Publish
13
+
14
+ ```bash
15
+ npm publish --access public
16
+ ```
@@ -0,0 +1,38 @@
1
+ {
2
+ "id": "@thibautrey/chatons-channel-telegram",
3
+ "name": "Telegram Channel",
4
+ "version": "1.0.0",
5
+ "kind": "channel",
6
+ "capabilities": [
7
+ "ui.mainView",
8
+ "storage.kv",
9
+ "queue.publish",
10
+ "queue.consume",
11
+ "events.subscribe",
12
+ "events.publish",
13
+ "host.notifications",
14
+ "host.conversations.read",
15
+ "host.conversations.write"
16
+ ],
17
+ "ui": {
18
+ "mainViews": [
19
+ {
20
+ "viewId": "telegram.main",
21
+ "title": "Telegram",
22
+ "webviewUrl": "chaton-extension://@thibautrey/chatons-channel-telegram/index.html",
23
+ "initialRoute": "/"
24
+ }
25
+ ]
26
+ },
27
+ "apis": {
28
+ "exposes": [
29
+ { "name": "channel.connect", "version": "1.0.0" },
30
+ { "name": "channel.disconnect", "version": "1.0.0" },
31
+ { "name": "channel.status", "version": "1.0.0" },
32
+ { "name": "channel.receive", "version": "1.0.0" },
33
+ { "name": "channel.send", "version": "1.0.0" },
34
+ { "name": "telegram.poll_once", "version": "1.0.0" },
35
+ { "name": "telegram.get_updates", "version": "1.0.0" }
36
+ ]
37
+ }
38
+ }
package/components.js ADDED
@@ -0,0 +1,108 @@
1
+ (function () {
2
+ var STYLE_ID = 'chaton-telegram-extension-style';
3
+
4
+ function ensureStyles() {
5
+ if (document.getElementById(STYLE_ID)) return;
6
+ var style = document.createElement('style');
7
+ style.id = STYLE_ID;
8
+ style.textContent = [
9
+ ':root {',
10
+ ' --ce-bg: hsl(220 12% 96%);',
11
+ ' --ce-fg: hsl(222 12% 14%);',
12
+ ' --ce-card: hsl(0 0% 100%);',
13
+ ' --ce-muted-fg: hsl(220 6% 44%);',
14
+ ' --ce-border: hsl(220 9% 85%);',
15
+ ' --ce-input: hsl(220 9% 85%);',
16
+ ' --ce-primary: hsl(220 7% 32%);',
17
+ ' --ce-primary-fg: hsl(0 0% 100%);',
18
+ ' --ce-accent: hsl(220 10% 93%);',
19
+ ' --ce-success: hsl(145 48% 42%);',
20
+ ' --ce-danger: hsl(0 62% 52%);',
21
+ '}',
22
+ ':root.dark {',
23
+ ' --ce-bg: hsl(220 24% 10%);',
24
+ ' --ce-fg: hsl(214 32% 93%);',
25
+ ' --ce-card: hsl(220 22% 14%);',
26
+ ' --ce-muted-fg: hsl(216 18% 72%);',
27
+ ' --ce-border: hsl(220 18% 24%);',
28
+ ' --ce-input: hsl(220 18% 24%);',
29
+ ' --ce-primary: hsl(214 30% 82%);',
30
+ ' --ce-primary-fg: hsl(220 24% 12%);',
31
+ ' --ce-accent: hsl(220 18% 20%);',
32
+ ' --ce-success: hsl(145 48% 58%);',
33
+ ' --ce-danger: hsl(0 72% 70%);',
34
+ '}',
35
+ '.ce-root, .ce-root * { box-sizing: border-box; }',
36
+ '.ce-root { min-height: 100vh; color: var(--ce-fg); background: var(--ce-bg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }',
37
+ '.ce-page { max-width: 1080px; margin: 0 auto; padding: 24px; display: grid; gap: 18px; }',
38
+ '.ce-header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; }',
39
+ '.ce-title { margin: 0; font-size: clamp(30px, 5vw, 42px); font-weight: 700; letter-spacing: -0.03em; }',
40
+ '.ce-subtitle { margin: 8px 0 0; color: var(--ce-muted-fg); line-height: 1.5; max-width: 760px; }',
41
+ '.ce-grid { display: grid; gap: 16px; }',
42
+ '.ce-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }',
43
+ '.ce-card { background: color-mix(in srgb, var(--ce-card) 86%, transparent); border: 1px solid color-mix(in srgb, var(--ce-border) 76%, transparent); border-radius: 24px; backdrop-filter: blur(16px); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08); }',
44
+ '.ce-card__body { padding: 20px; display: grid; gap: 14px; }',
45
+ '.ce-section-title { margin: 0; font-size: 18px; font-weight: 650; }',
46
+ '.ce-copy { margin: 0; color: var(--ce-muted-fg); font-size: 14px; line-height: 1.5; }',
47
+ '.ce-field { display: grid; gap: 6px; }',
48
+ '.ce-label { font-size: 13px; font-weight: 650; }',
49
+ '.ce-input, .ce-textarea, .ce-select { width: 100%; border: 1px solid var(--ce-input); background: var(--ce-card); color: var(--ce-fg); border-radius: 14px; padding: 11px 12px; font: inherit; }',
50
+ '.ce-textarea { min-height: 100px; resize: vertical; }',
51
+ '.ce-button { display: inline-flex; align-items: center; justify-content: center; min-height: 42px; border-radius: 14px; border: 1px solid transparent; padding: 0 16px; font-size: 14px; font-weight: 650; cursor: pointer; }',
52
+ '.ce-button--default { background: var(--ce-primary); color: var(--ce-primary-fg); }',
53
+ '.ce-button--outline { background: var(--ce-card); color: var(--ce-fg); border-color: var(--ce-border); }',
54
+ '.ce-button--ghost { background: transparent; color: var(--ce-muted-fg); }',
55
+ '.ce-inline { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }',
56
+ '.ce-badge { display: inline-flex; align-items: center; min-height: 24px; border-radius: 999px; padding: 0 10px; font-size: 12px; font-weight: 700; border: 1px solid var(--ce-border); }',
57
+ '.ce-badge--success { color: var(--ce-success); }',
58
+ '.ce-badge--danger { color: var(--ce-danger); }',
59
+ '.ce-badge--muted { color: var(--ce-muted-fg); }',
60
+ '.ce-list { display: grid; gap: 10px; }',
61
+ '.ce-row { display: flex; justify-content: space-between; gap: 12px; align-items: flex-start; border: 1px solid var(--ce-border); background: color-mix(in srgb, var(--ce-card) 72%, transparent); border-radius: 16px; padding: 14px; }',
62
+ '.ce-row__title { margin: 0; font-size: 15px; font-weight: 650; }',
63
+ '.ce-row__meta { margin: 4px 0 0; color: var(--ce-muted-fg); font-size: 13px; line-height: 1.45; white-space: pre-wrap; }',
64
+ '.ce-note { border-left: 3px solid var(--ce-border); padding-left: 12px; color: var(--ce-muted-fg); font-size: 13px; line-height: 1.5; }',
65
+ '@media (max-width: 900px) { .ce-grid--2 { grid-template-columns: 1fr; } .ce-header { flex-direction: column; } }'
66
+ ].join('\n');
67
+ document.head.appendChild(style);
68
+ }
69
+
70
+ function el(tag, className, text) {
71
+ var node = document.createElement(tag);
72
+ if (className) node.className = className;
73
+ if (typeof text === 'string') node.textContent = text;
74
+ return node;
75
+ }
76
+
77
+ function createButton(opts) {
78
+ var node = el('button', 'ce-button ce-button--' + ((opts && opts.variant) || 'default'));
79
+ node.type = (opts && opts.type) || 'button';
80
+ node.textContent = (opts && opts.text) || '';
81
+ return node;
82
+ }
83
+
84
+ function createCard() {
85
+ var root = el('section', 'ce-card');
86
+ var body = el('div', 'ce-card__body');
87
+ root.appendChild(body);
88
+ return { root: root, body: body };
89
+ }
90
+
91
+ function createField(labelText, input, help) {
92
+ var wrap = el('div', 'ce-field');
93
+ var label = el('label', 'ce-label', labelText);
94
+ if (input && input.id) label.htmlFor = input.id;
95
+ wrap.appendChild(label);
96
+ wrap.appendChild(input);
97
+ if (help) wrap.appendChild(el('div', 'ce-copy', help));
98
+ return wrap;
99
+ }
100
+
101
+ window.telegramChannelUi = {
102
+ ensureStyles: ensureStyles,
103
+ el: el,
104
+ createButton: createButton,
105
+ createCard: createCard,
106
+ createField: createField,
107
+ };
108
+ })();
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Telegram Channel</title>
7
+ </head>
8
+ <body>
9
+ <div id="app" class="ce-root"></div>
10
+ <script src="./components.js"></script>
11
+ <script src="./index.js"></script>
12
+ </body>
13
+ </html>
package/index.js ADDED
@@ -0,0 +1,474 @@
1
+ (function () {
2
+ var EXTENSION_ID = '@chaton/channel-telegram';
3
+ var ui = window.telegramChannelUi;
4
+ if (!ui) throw new Error('telegramChannelUi is required');
5
+ ui.ensureStyles();
6
+
7
+ function syncThemeClass() {
8
+ var root = document.documentElement;
9
+ if (!root) return;
10
+ root.classList.toggle('dark', !!(window.parent && window.parent.document && window.parent.document.documentElement && window.parent.document.documentElement.classList.contains('dark')));
11
+ }
12
+ syncThemeClass();
13
+
14
+ var MODEL_KEY = 'chatons:telegram-channel:model';
15
+ var app = document.getElementById('app');
16
+ var state = {
17
+ status: null,
18
+ log: [],
19
+ updates: [],
20
+ conversations: [],
21
+ allModels: [],
22
+ modelPicker: null,
23
+ };
24
+
25
+ function log(msg) {
26
+ state.log.unshift({ at: new Date().toISOString(), message: String(msg || '') });
27
+ state.log = state.log.slice(0, 20);
28
+ renderLog();
29
+ }
30
+
31
+ function relTime(iso) {
32
+ var ts = Date.parse(iso);
33
+ if (!Number.isFinite(ts)) return 'unknown';
34
+ var diff = Math.max(0, Date.now() - ts);
35
+ var sec = Math.floor(diff / 1000);
36
+ if (sec < 60) return sec + 's';
37
+ var min = Math.floor(sec / 60);
38
+ if (min < 60) return min + 'm';
39
+ var h = Math.floor(min / 60);
40
+ if (h < 24) return h + 'h';
41
+ return Math.floor(h / 24) + 'd';
42
+ }
43
+
44
+ function extensionCall(api, payload) {
45
+ return window.chaton.extensionCall('chatons-ui', EXTENSION_ID, api, '^1.0.0', payload);
46
+ }
47
+
48
+ async function kvGet(key) {
49
+ var res = await window.chaton.extensionStorageKvGet(EXTENSION_ID, key);
50
+ return res && res.ok ? res.data : null;
51
+ }
52
+
53
+ async function kvSet(key, value) {
54
+ return window.chaton.extensionStorageKvSet(EXTENSION_ID, key, value);
55
+ }
56
+
57
+ async function hostNotify(title, body) {
58
+ return window.chaton.extensionHostCall(EXTENSION_ID, 'notifications.notify', { title: title, body: body });
59
+ }
60
+
61
+ async function getStatus() {
62
+ var statusRes = await extensionCall('channel.status', {});
63
+ if (statusRes && statusRes.ok) {
64
+ state.status = statusRes.data || null;
65
+ } else {
66
+ state.status = null;
67
+ }
68
+ renderStatus();
69
+ }
70
+
71
+ async function loadConversations() {
72
+ var res = await window.chaton.extensionHostCall(EXTENSION_ID, 'conversations.list', {});
73
+ state.conversations = res && res.ok && Array.isArray(res.data) ? res.data : [];
74
+ }
75
+
76
+ async function fetchTelegramUpdates(token, offset, limit) {
77
+ var url = 'https://api.telegram.org/bot' + encodeURIComponent(token) + '/getUpdates?timeout=1&allowed_updates=%5B%22message%22%5D';
78
+ if (typeof offset === 'number' && Number.isFinite(offset)) url += '&offset=' + encodeURIComponent(String(offset));
79
+ if (typeof limit === 'number' && Number.isFinite(limit)) url += '&limit=' + encodeURIComponent(String(limit));
80
+ var response = await fetch(url);
81
+ var json = await response.json();
82
+ return json;
83
+ }
84
+
85
+ function splitModelKey(modelKey) {
86
+ if (typeof modelKey !== 'string') return { provider: null, modelId: null };
87
+ var idx = modelKey.indexOf('/');
88
+ if (idx <= 0 || idx >= modelKey.length - 1) return { provider: null, modelId: null };
89
+ return { provider: modelKey.slice(0, idx), modelId: modelKey.slice(idx + 1) };
90
+ }
91
+
92
+ async function sendTelegramMessage(token, chatId, text) {
93
+ var response = await fetch('https://api.telegram.org/bot' + encodeURIComponent(token) + '/sendMessage', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ chat_id: chatId, text: text })
97
+ });
98
+ return response.json();
99
+ }
100
+
101
+ function buildShell() {
102
+ app.innerHTML = '';
103
+ var page = ui.el('div', 'ce-page');
104
+ var header = ui.el('div', 'ce-header');
105
+ var headerText = ui.el('div', '');
106
+ headerText.appendChild(ui.el('h1', 'ce-title', 'Telegram'));
107
+ headerText.appendChild(ui.el('p', 'ce-subtitle', 'Connecte un bot Telegram a Chatons. Cette extension gere la configuration, le polling, le statut, et les tests d\'envoi. En l\'etat actuel du host, l\'injection directe des messages Telegram dans les threads Chatons reste limitee par l\'absence d\'une API host dediee d\'ingestion externe.'));
108
+ header.appendChild(headerText);
109
+ page.appendChild(header);
110
+
111
+ var grid = ui.el('div', 'ce-grid ce-grid--2');
112
+
113
+ var configCard = ui.createCard();
114
+ configCard.body.appendChild(ui.el('h2', 'ce-section-title', 'Configuration'));
115
+ configCard.body.appendChild(ui.el('p', 'ce-copy', 'Renseignez le token du bot Telegram et les options de polling.'));
116
+ var tokenInput = ui.el('input', 'ce-input');
117
+ tokenInput.id = 'token';
118
+ tokenInput.placeholder = '123456:ABCDEF...';
119
+ var usernameInput = ui.el('input', 'ce-input');
120
+ usernameInput.id = 'botUsername';
121
+ usernameInput.placeholder = 'my_chaton_bot';
122
+ var pollLimitInput = ui.el('input', 'ce-input');
123
+ pollLimitInput.id = 'pollLimit';
124
+ pollLimitInput.type = 'number';
125
+ pollLimitInput.min = '1';
126
+ pollLimitInput.value = '10';
127
+ var modelPickerHost = ui.el('div', '');
128
+ modelPickerHost.id = 'modelPickerHost';
129
+ var testChatIdInput = ui.el('input', 'ce-input');
130
+ testChatIdInput.id = 'testChatId';
131
+ testChatIdInput.placeholder = '123456789';
132
+ var testMessageInput = ui.el('textarea', 'ce-textarea');
133
+ testMessageInput.id = 'testMessage';
134
+ testMessageInput.value = 'Hello from Chatons Telegram Channel';
135
+
136
+ configCard.body.appendChild(ui.createField('Bot token', tokenInput, 'Stocke dans l\'espace KV de l\'extension.'));
137
+ configCard.body.appendChild(ui.createField('Bot username', usernameInput, 'Optionnel, pour affichage et verification.'));
138
+ configCard.body.appendChild(ui.createField('Modele Chatons', modelPickerHost, 'Utilise le model selector de Chatons pour choisir le modele du channel Telegram.'));
139
+ configCard.body.appendChild(ui.createField('Polling limit', pollLimitInput, 'Nombre max d\'updates recuperes par appel.'));
140
+ configCard.body.appendChild(ui.createField('Chat ID de test', testChatIdInput, 'Utilise pour envoyer un message de test.'));
141
+ configCard.body.appendChild(ui.createField('Message de test', testMessageInput));
142
+
143
+ var configActions = ui.el('div', 'ce-inline');
144
+ var saveBtn = ui.createButton({ text: 'Sauvegarder', variant: 'default' });
145
+ var connectBtn = ui.createButton({ text: 'Connecter', variant: 'outline' });
146
+ var disconnectBtn = ui.createButton({ text: 'Deconnecter', variant: 'ghost' });
147
+ var pollBtn = ui.createButton({ text: 'Poll une fois', variant: 'outline' });
148
+ var testSendBtn = ui.createButton({ text: 'Envoyer test', variant: 'outline' });
149
+ configActions.appendChild(saveBtn);
150
+ configActions.appendChild(connectBtn);
151
+ configActions.appendChild(disconnectBtn);
152
+ configActions.appendChild(pollBtn);
153
+ configActions.appendChild(testSendBtn);
154
+ configCard.body.appendChild(configActions);
155
+
156
+ var statusCard = ui.createCard();
157
+ statusCard.body.appendChild(ui.el('h2', 'ce-section-title', 'Statut'));
158
+ statusCard.body.appendChild(ui.el('p', 'ce-copy', 'Connexion, offsets Telegram, conversations mappees et diagnostics.'));
159
+ var statusWrap = ui.el('div', 'ce-grid');
160
+ statusWrap.id = 'status';
161
+ statusCard.body.appendChild(statusWrap);
162
+
163
+ grid.appendChild(configCard.root);
164
+ grid.appendChild(statusCard.root);
165
+
166
+ var updatesCard = ui.createCard();
167
+ updatesCard.body.appendChild(ui.el('h2', 'ce-section-title', 'Dernieres updates Telegram'));
168
+ updatesCard.body.appendChild(ui.el('p', 'ce-copy', 'Les messages entrants detectes par polling apparaissent ici.'));
169
+ var updatesList = ui.el('div', 'ce-list');
170
+ updatesList.id = 'updates';
171
+ updatesCard.body.appendChild(updatesList);
172
+
173
+ var logCard = ui.createCard();
174
+ logCard.body.appendChild(ui.el('h2', 'ce-section-title', 'Journal'));
175
+ logCard.body.appendChild(ui.el('p', 'ce-copy', 'Trace locale de l\'extension.'));
176
+ var logList = ui.el('div', 'ce-list');
177
+ logList.id = 'log';
178
+ logCard.body.appendChild(logList);
179
+
180
+ page.appendChild(grid);
181
+ page.appendChild(updatesCard.root);
182
+ page.appendChild(logCard.root);
183
+ app.appendChild(page);
184
+
185
+ return {
186
+ tokenInput: tokenInput,
187
+ usernameInput: usernameInput,
188
+ pollLimitInput: pollLimitInput,
189
+ modelPickerHost: modelPickerHost,
190
+ testChatIdInput: testChatIdInput,
191
+ testMessageInput: testMessageInput,
192
+ saveBtn: saveBtn,
193
+ connectBtn: connectBtn,
194
+ disconnectBtn: disconnectBtn,
195
+ pollBtn: pollBtn,
196
+ testSendBtn: testSendBtn,
197
+ statusWrap: statusWrap,
198
+ updatesList: updatesList,
199
+ logList: logList,
200
+ };
201
+ }
202
+
203
+ var refs = buildShell();
204
+
205
+ function renderStatus() {
206
+ refs.statusWrap.innerHTML = '';
207
+ var s = state.status;
208
+ if (!s) {
209
+ refs.statusWrap.appendChild(ui.el('div', 'ce-note', 'Aucun statut charge.'));
210
+ return;
211
+ }
212
+ var rows = [
213
+ ['Connexion', s.connected ? 'connecte' : 'deconnecte'],
214
+ ['Provider', s.provider || 'telegram'],
215
+ ['Bot', s.account && s.account.label ? s.account.label : 'n/a'],
216
+ ['Mapped threads', String((s.counters && s.counters.mappedThreads) || 0)],
217
+ ['Pending inbound', String((s.counters && s.counters.pendingInbound) || 0)],
218
+ ['Pending outbound', String((s.counters && s.counters.pendingOutbound) || 0)],
219
+ ['Dead letters', String((s.counters && s.counters.deadLetters) || 0)],
220
+ ['Last inbound', s.lastInboundAt ? relTime(s.lastInboundAt) : 'never'],
221
+ ['Last outbound', s.lastOutboundAt ? relTime(s.lastOutboundAt) : 'never'],
222
+ ['Last update offset', String(s.lastUpdateOffset || 0)],
223
+ ['Modele', s.modelKey || 'non selectionne'],
224
+ ['Bridge limitation', s.bridgeReady ? 'host bridge available' : 'host bridge missing'],
225
+ ];
226
+ rows.forEach(function (entry) {
227
+ var row = ui.el('div', 'ce-row');
228
+ var left = ui.el('div', '');
229
+ left.appendChild(ui.el('p', 'ce-row__title', entry[0]));
230
+ row.appendChild(left);
231
+ row.appendChild(ui.el('div', 'ce-row__meta', entry[1]));
232
+ refs.statusWrap.appendChild(row);
233
+ });
234
+ }
235
+
236
+ function renderUpdates() {
237
+ refs.updatesList.innerHTML = '';
238
+ if (!state.updates.length) {
239
+ refs.updatesList.appendChild(ui.el('div', 'ce-note', 'Aucune update Telegram recente.'));
240
+ return;
241
+ }
242
+ state.updates.forEach(function (update) {
243
+ var row = ui.el('div', 'ce-row');
244
+ var left = ui.el('div', '');
245
+ left.appendChild(ui.el('p', 'ce-row__title', 'Update #' + update.update_id + ' - chat ' + (update.chat_id || 'unknown')));
246
+ left.appendChild(ui.el('div', 'ce-row__meta', (update.text || '[no text]') + '\nfrom: ' + (update.from_name || 'unknown') + ' (' + (update.from_id || 'n/a') + ')'));
247
+ row.appendChild(left);
248
+ row.appendChild(ui.el('div', 'ce-row__meta', update.receivedAt ? relTime(update.receivedAt) : 'now'));
249
+ refs.updatesList.appendChild(row);
250
+ });
251
+ }
252
+
253
+ function renderLog() {
254
+ refs.logList.innerHTML = '';
255
+ if (!state.log.length) {
256
+ refs.logList.appendChild(ui.el('div', 'ce-note', 'Aucun evenement journalise.'));
257
+ return;
258
+ }
259
+ state.log.forEach(function (entry) {
260
+ var row = ui.el('div', 'ce-row');
261
+ var left = ui.el('div', '');
262
+ left.appendChild(ui.el('p', 'ce-row__title', entry.message));
263
+ left.appendChild(ui.el('div', 'ce-row__meta', entry.at));
264
+ row.appendChild(left);
265
+ refs.logList.appendChild(row);
266
+ });
267
+ }
268
+
269
+ async function loadModels() {
270
+ if (!window.chatonUi || typeof window.chatonUi.createModelPicker !== 'function') return;
271
+ var res = await window.chaton.listPiModels();
272
+ if (!res || !res.ok) return;
273
+ state.allModels = Array.isArray(res.models) ? res.models : [];
274
+ if (state.modelPicker) {
275
+ state.modelPicker.setModels(state.allModels);
276
+ var cfg = await kvGet('telegram.config');
277
+ var saved = cfg && typeof cfg === 'object' && typeof cfg.modelKey === 'string' ? cfg.modelKey : localStorage.getItem(MODEL_KEY);
278
+ state.modelPicker.setSelected(saved || null);
279
+ var selected = state.modelPicker.getSelected();
280
+ if (selected) localStorage.setItem(MODEL_KEY, selected);
281
+ }
282
+ }
283
+
284
+ async function loadConfigIntoForm() {
285
+ var cfg = await kvGet('telegram.config');
286
+ cfg = cfg && typeof cfg === 'object' ? cfg : {};
287
+ refs.tokenInput.value = cfg.token || '';
288
+ refs.usernameInput.value = cfg.botUsername || '';
289
+ refs.pollLimitInput.value = String(cfg.pollLimit || 10);
290
+ refs.testChatIdInput.value = cfg.testChatId || '';
291
+ refs.testMessageInput.value = cfg.testMessage || 'Hello from Chatons Telegram Channel';
292
+ if (state.modelPicker) {
293
+ state.modelPicker.setSelected(typeof cfg.modelKey === 'string' ? cfg.modelKey : (localStorage.getItem(MODEL_KEY) || null));
294
+ }
295
+ state.updates = Array.isArray(cfg.lastUpdates) ? cfg.lastUpdates : [];
296
+ renderUpdates();
297
+ }
298
+
299
+ async function saveConfigFromForm() {
300
+ var modelKey = state.modelPicker ? state.modelPicker.getSelected() : null;
301
+ if (modelKey) localStorage.setItem(MODEL_KEY, modelKey);
302
+ var cfg = {
303
+ token: String(refs.tokenInput.value || '').trim(),
304
+ botUsername: String(refs.usernameInput.value || '').trim(),
305
+ modelKey: modelKey,
306
+ pollLimit: Math.max(1, Number(refs.pollLimitInput.value || 10) || 10),
307
+ testChatId: String(refs.testChatIdInput.value || '').trim(),
308
+ testMessage: String(refs.testMessageInput.value || '').trim(),
309
+ };
310
+ await kvSet('telegram.config', cfg);
311
+ log('Configuration sauvegardee');
312
+ return cfg;
313
+ }
314
+
315
+ async function processTelegramUpdatesFromApi(result) {
316
+ var cfg = await kvGet('telegram.config');
317
+ cfg = cfg && typeof cfg === 'object' ? cfg : {};
318
+ var normalized = [];
319
+ var updates = result && result.ok && Array.isArray(result.result) ? result.result : [];
320
+ var maxUpdateId = Number(cfg.lastUpdateOffset || 0);
321
+
322
+ for (var i = 0; i < updates.length; i += 1) {
323
+ var update = updates[i] || {};
324
+ var message = update.message || {};
325
+ var chat = message.chat || {};
326
+ var from = message.from || {};
327
+ var text = typeof message.text === 'string' ? message.text : '';
328
+ var updateId = Number(update.update_id || 0);
329
+ if (updateId > maxUpdateId) maxUpdateId = updateId;
330
+ if (!text.trim()) continue;
331
+
332
+ var item = {
333
+ update_id: updateId,
334
+ message_id: String(message.message_id || updateId || ''),
335
+ chat_id: String(chat.id || ''),
336
+ from_id: String(from.id || ''),
337
+ from_name: [from.first_name, from.last_name].filter(Boolean).join(' ').trim() || from.username || 'unknown',
338
+ text: text,
339
+ receivedAt: new Date().toISOString(),
340
+ };
341
+ normalized.push(item);
342
+
343
+ var mappingKey = 'telegram:' + item.chat_id;
344
+ var threadRes = await window.chaton.extensionHostCall(EXTENSION_ID, 'channels.upsertGlobalThread', {
345
+ mappingKey: mappingKey,
346
+ title: item.from_name ? ('Telegram - ' + item.from_name) : ('Telegram - ' + item.chat_id),
347
+ modelKey: cfg.modelKey || null,
348
+ });
349
+ if (!threadRes || !threadRes.ok || !threadRes.data || !threadRes.data.conversation || !threadRes.data.conversation.id) {
350
+ log('Impossible de creer/resoudre le thread global pour chat ' + item.chat_id);
351
+ continue;
352
+ }
353
+
354
+ var ingestRes = await window.chaton.extensionHostCall(EXTENSION_ID, 'channels.ingestMessage', {
355
+ conversationId: threadRes.data.conversation.id,
356
+ message: item.text,
357
+ idempotencyKey: item.message_id,
358
+ metadata: {
359
+ remoteThreadId: item.chat_id,
360
+ remoteUserId: item.from_id,
361
+ remoteUserName: item.from_name,
362
+ provider: 'telegram',
363
+ source: 'poll',
364
+ updateId: item.update_id,
365
+ },
366
+ });
367
+
368
+ if (ingestRes && ingestRes.ok) {
369
+ log('Message importe dans Chatons pour chat ' + item.chat_id);
370
+ if (typeof ingestRes.data.reply === 'string' && ingestRes.data.reply.trim() && cfg.token) {
371
+ try {
372
+ await sendTelegramMessage(cfg.token, item.chat_id, ingestRes.data.reply.trim());
373
+ log('Reponse Chatons renvoyee vers Telegram pour chat ' + item.chat_id);
374
+ } catch (error) {
375
+ log('Echec d\'envoi de la reponse Telegram: ' + (error && error.message ? error.message : String(error)));
376
+ }
377
+ }
378
+ } else {
379
+ log('Echec d\'import Chatons pour chat ' + item.chat_id);
380
+ }
381
+ }
382
+
383
+ cfg.lastUpdateOffset = maxUpdateId > 0 ? (maxUpdateId + 1) : Number(cfg.lastUpdateOffset || 0);
384
+ cfg.lastUpdates = normalized.slice(0, 50);
385
+ await kvSet('telegram.config', cfg);
386
+ state.updates = cfg.lastUpdates;
387
+ renderUpdates();
388
+ return normalized;
389
+ }
390
+
391
+ refs.saveBtn.addEventListener('click', async function () {
392
+ await saveConfigFromForm();
393
+ await hostNotify('Telegram', 'Configuration sauvegardee.');
394
+ });
395
+
396
+ refs.connectBtn.addEventListener('click', async function () {
397
+ var cfg = await saveConfigFromForm();
398
+ var res = await extensionCall('channel.connect', { config: cfg, interactive: true });
399
+ log(res && res.ok ? 'Connexion Telegram reussie' : 'Connexion Telegram echouee');
400
+ await getStatus();
401
+ });
402
+
403
+ refs.disconnectBtn.addEventListener('click', async function () {
404
+ var res = await extensionCall('channel.disconnect', {});
405
+ log(res && res.ok ? 'Deconnexion Telegram effectuee' : 'Deconnexion Telegram echouee');
406
+ await getStatus();
407
+ });
408
+
409
+ refs.pollBtn.addEventListener('click', async function () {
410
+ var cfg = await saveConfigFromForm();
411
+ if (!cfg.token) {
412
+ log('Polling Telegram impossible: token absent');
413
+ return;
414
+ }
415
+ try {
416
+ var apiResult = await fetchTelegramUpdates(cfg.token, Number(cfg.lastUpdateOffset || 0) || 0, cfg.pollLimit || 10);
417
+ await processTelegramUpdatesFromApi(apiResult);
418
+ log('Polling Telegram termine');
419
+ } catch (error) {
420
+ log('Polling Telegram echoue: ' + (error && error.message ? error.message : String(error)));
421
+ }
422
+ await getStatus();
423
+ });
424
+
425
+ refs.testSendBtn.addEventListener('click', async function () {
426
+ var cfg = await saveConfigFromForm();
427
+ if (!cfg.token) {
428
+ log('Envoi test impossible: token absent');
429
+ return;
430
+ }
431
+ try {
432
+ var sendResult = await sendTelegramMessage(cfg.token, String(refs.testChatIdInput.value || '').trim(), String(refs.testMessageInput.value || '').trim());
433
+ log(sendResult && sendResult.ok ? 'Message test envoye' : 'Envoi test echoue');
434
+ } catch (error) {
435
+ log('Envoi test echoue: ' + (error && error.message ? error.message : String(error)));
436
+ }
437
+ await getStatus();
438
+ });
439
+
440
+ window.addEventListener('message', function (event) {
441
+ var data = event && event.data;
442
+ if (!data || data.type !== 'chaton.extension.deeplink') return;
443
+ var payload = data.payload || {};
444
+ if (payload.viewId !== 'telegram.main') return;
445
+ if (payload.target === 'open-create' || payload.target === 'open-config') {
446
+ refs.tokenInput.focus();
447
+ }
448
+ });
449
+
450
+ if (window.chatonUi && typeof window.chatonUi.createModelPicker === 'function') {
451
+ state.modelPicker = window.chatonUi.createModelPicker({
452
+ host: refs.modelPickerHost,
453
+ onChange: function (modelKey) {
454
+ if (modelKey) localStorage.setItem(MODEL_KEY, modelKey);
455
+ },
456
+ labels: {
457
+ filterPlaceholder: 'Filtrer les modeles...',
458
+ more: 'plus',
459
+ scopedOnly: 'scoped uniquement',
460
+ noScoped: 'Aucun modele scoped',
461
+ noModels: 'Aucun modele disponible'
462
+ }
463
+ });
464
+ }
465
+
466
+ (async function init() {
467
+ await loadConversations();
468
+ await loadModels();
469
+ await loadConfigIntoForm();
470
+ await getStatus();
471
+ renderLog();
472
+ log('Extension Telegram initialisee');
473
+ })();
474
+ })();
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@thibautrey/chatons-channel-telegram",
3
+ "version": "1.0.0",
4
+ "description": "Telegram channel extension for Chatons",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "files": [
8
+ "chaton.extension.json",
9
+ "index.html",
10
+ "index.js",
11
+ "components.js"
12
+ ],
13
+ "keywords": [
14
+ "chatons",
15
+ "telegram",
16
+ "extension",
17
+ "channel"
18
+ ]
19
+ }