telecodex 0.1.3 → 0.1.4

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.
@@ -0,0 +1,183 @@
1
+ import { FormattedString } from "@grammyjs/parse-mode";
2
+ import { retryTelegramCall } from "./delivery.js";
3
+ import { TELEGRAM_SAFE_TEXT_LIMIT } from "./splitMessage.js";
4
+ export function textField(label, value) {
5
+ return {
6
+ label,
7
+ value,
8
+ style: "plain",
9
+ };
10
+ }
11
+ export function codeField(label, value) {
12
+ return {
13
+ label,
14
+ value,
15
+ style: "code",
16
+ };
17
+ }
18
+ export function renderReplyDocument(document) {
19
+ const lines = [formatTitle(document.title)];
20
+ if (document.fields?.length) {
21
+ lines.push(...document.fields.map(formatField));
22
+ }
23
+ for (const section of document.sections ?? []) {
24
+ appendBlankLine(lines);
25
+ if (section.title) {
26
+ lines.push(formatTitle(section.title));
27
+ }
28
+ if (section.fields?.length) {
29
+ lines.push(...section.fields.map(formatField));
30
+ }
31
+ if (section.lines?.length) {
32
+ lines.push(...section.lines);
33
+ }
34
+ }
35
+ if (document.footer != null) {
36
+ appendBlankLine(lines);
37
+ lines.push(...normalizeLines(document.footer));
38
+ }
39
+ return joinLines(trimBlankLines(lines));
40
+ }
41
+ export async function replyDocument(ctx, document) {
42
+ await replyFormatted(ctx, renderReplyDocument(document));
43
+ }
44
+ export async function sendReplyDocument(bot, target, document, logger) {
45
+ await sendFormatted(bot, target, renderReplyDocument(document), logger);
46
+ }
47
+ export async function replyNotice(ctx, content) {
48
+ await replyFormatted(ctx, renderNotice(content));
49
+ }
50
+ export async function sendReplyNotice(bot, target, content, logger) {
51
+ await sendFormatted(bot, target, renderNotice(content), logger);
52
+ }
53
+ export async function replyError(ctx, message, detail) {
54
+ await replyNotice(ctx, detail == null ? message : [message, ...normalizeLines(detail)]);
55
+ }
56
+ export async function sendReplyError(bot, target, message, detail, logger) {
57
+ await sendReplyNotice(bot, target, detail == null ? message : [message, ...normalizeLines(detail)], logger);
58
+ }
59
+ export async function replyUsage(ctx, usage) {
60
+ await replyNotice(ctx, Array.isArray(usage) ? ["Usage:", ...usage] : `Usage: ${usage}`);
61
+ }
62
+ export async function sendReplyUsage(bot, target, usage, logger) {
63
+ await sendReplyNotice(bot, target, Array.isArray(usage) ? ["Usage:", ...usage] : `Usage: ${usage}`, logger);
64
+ }
65
+ export async function replyFormatted(ctx, message) {
66
+ const target = targetFromContext(ctx);
67
+ if (target && typeof ctx.api?.sendMessage === "function") {
68
+ await sendFormattedViaApi(ctx.api, ctx.api, target, message);
69
+ return;
70
+ }
71
+ await ctx.reply(message.text, {
72
+ ...toMessageOptions(message),
73
+ });
74
+ }
75
+ export async function sendFormatted(bot, target, message, logger) {
76
+ await sendFormattedViaApi(bot.api, bot.api, target, message, logger);
77
+ }
78
+ function renderNotice(content) {
79
+ return joinLines(trimBlankLines(normalizeLines(content)));
80
+ }
81
+ function toMessageOptions(message) {
82
+ return {
83
+ ...(message.entities && message.entities.length > 0 ? { entities: message.entities } : {}),
84
+ link_preview_options: { is_disabled: true },
85
+ };
86
+ }
87
+ async function sendFormattedViaApi(api, cooldownKey, target, message, logger) {
88
+ for (const chunk of splitTextWithEntities(message)) {
89
+ await retryTelegramCall(cooldownKey, () => api.sendMessage(target.chatId, chunk.text, {
90
+ ...(target.messageThreadId == null ? {} : { message_thread_id: target.messageThreadId }),
91
+ ...toMessageOptions(chunk),
92
+ }), logger, "telegram send rate limited", {
93
+ chatId: target.chatId,
94
+ messageThreadId: target.messageThreadId,
95
+ });
96
+ }
97
+ }
98
+ function splitTextWithEntities(message, limit = TELEGRAM_SAFE_TEXT_LIMIT) {
99
+ if (message.text.length <= limit) {
100
+ return [message];
101
+ }
102
+ const chunks = [];
103
+ let start = 0;
104
+ while (start < message.text.length) {
105
+ const remaining = message.text.length - start;
106
+ const end = remaining <= limit
107
+ ? message.text.length
108
+ : start + chooseSplitPoint(message.text.slice(start, start + limit), limit);
109
+ chunks.push(sliceTextWithEntities(message, start, Math.max(start + 1, end)));
110
+ start = Math.max(start + 1, end);
111
+ }
112
+ return chunks;
113
+ }
114
+ function sliceTextWithEntities(message, start, end) {
115
+ const text = message.text.slice(start, end);
116
+ const entities = message.entities?.flatMap((entity) => {
117
+ const overlapStart = Math.max(entity.offset, start);
118
+ const overlapEnd = Math.min(entity.offset + entity.length, end);
119
+ if (overlapStart >= overlapEnd)
120
+ return [];
121
+ return [
122
+ {
123
+ ...entity,
124
+ offset: overlapStart - start,
125
+ length: overlapEnd - overlapStart,
126
+ },
127
+ ];
128
+ });
129
+ return entities && entities.length > 0
130
+ ? { text, entities }
131
+ : { text };
132
+ }
133
+ function chooseSplitPoint(text, limit) {
134
+ if (text.length <= limit)
135
+ return text.length;
136
+ const slice = text.slice(0, limit);
137
+ const splitAt = Math.max(slice.lastIndexOf("\n\n"), slice.lastIndexOf("\n"), slice.lastIndexOf(" "));
138
+ return splitAt > limit * 0.55 ? splitAt : limit;
139
+ }
140
+ function targetFromContext(ctx) {
141
+ const chatId = ctx.chat?.id;
142
+ if (chatId == null)
143
+ return null;
144
+ return {
145
+ chatId,
146
+ messageThreadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id ?? null,
147
+ };
148
+ }
149
+ function formatTitle(value) {
150
+ return FormattedString.bold(value);
151
+ }
152
+ function formatField(field) {
153
+ const value = formatValue(field.value);
154
+ if (field.style === "code") {
155
+ return FormattedString.join([`${field.label}: `, FormattedString.code(value)]);
156
+ }
157
+ return `${field.label}: ${value}`;
158
+ }
159
+ function joinLines(lines) {
160
+ return FormattedString.join(lines, "\n");
161
+ }
162
+ function appendBlankLine(lines) {
163
+ if (lines.length === 0 || lines.at(-1) === "")
164
+ return;
165
+ lines.push("");
166
+ }
167
+ function trimBlankLines(lines) {
168
+ let start = 0;
169
+ let end = lines.length;
170
+ while (lines[start] === "")
171
+ start += 1;
172
+ while (lines[end - 1] === "")
173
+ end -= 1;
174
+ return lines.slice(start, end);
175
+ }
176
+ function normalizeLines(lines) {
177
+ return Array.isArray(lines) ? lines : [lines];
178
+ }
179
+ function formatValue(value) {
180
+ if (value == null)
181
+ return "none";
182
+ return String(value);
183
+ }
@@ -98,6 +98,16 @@ export class MessageBuffer {
98
98
  this.states.delete(from);
99
99
  this.states.set(to, state);
100
100
  }
101
+ dispose() {
102
+ for (const state of this.states.values()) {
103
+ if (state.timer) {
104
+ clearTimeout(state.timer);
105
+ state.timer = null;
106
+ }
107
+ this.stopActivityPulse(state);
108
+ }
109
+ this.states.clear();
110
+ }
101
111
  async complete(key, finalMarkdown) {
102
112
  const state = this.states.get(key);
103
113
  if (!state)
@@ -1,4 +1,4 @@
1
- const TELEGRAM_SAFE_TEXT_LIMIT = 3900;
1
+ export const TELEGRAM_SAFE_TEXT_LIMIT = 3900;
2
2
  export function splitTelegramText(text, limit = TELEGRAM_SAFE_TEXT_LIMIT) {
3
3
  if (text.length <= limit)
4
4
  return [text];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telecodex",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Telegram bridge for local Codex.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,13 +49,15 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@clack/prompts": "^0.11.0",
52
+ "@grammyjs/parse-mode": "^2.3.0",
52
53
  "@grammyjs/runner": "^2.0.3",
53
54
  "@napi-rs/keyring": "^1.2.0",
54
55
  "@openai/codex-sdk": "^0.120.0",
55
56
  "clipboardy": "^4.0.0",
56
- "grammy": "^1.36.3",
57
+ "grammy": "^1.42.0",
57
58
  "markdown-it": "^14.1.0",
58
- "pino": "^10.3.1"
59
+ "pino": "^10.3.1",
60
+ "write-file-atomic": "^7.0.1"
59
61
  },
60
62
  "devDependencies": {
61
63
  "@types/markdown-it": "^14.1.2",