@adriangalilea/utils 0.5.0 → 0.7.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 +46 -0
- package/dist/bot/access-control.d.ts +339 -0
- package/dist/bot/access-control.d.ts.map +1 -0
- package/dist/bot/access-control.js +516 -0
- package/dist/bot/access-control.js.map +1 -0
- package/dist/bot/coalesce.d.ts +107 -0
- package/dist/bot/coalesce.d.ts.map +1 -0
- package/dist/bot/coalesce.js +155 -0
- package/dist/bot/coalesce.js.map +1 -0
- package/dist/bot/index.d.ts +18 -0
- package/dist/bot/index.d.ts.map +1 -0
- package/dist/bot/index.js +18 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/bot/kit.d.ts +50 -0
- package/dist/bot/kit.d.ts.map +1 -0
- package/dist/bot/kit.js +52 -0
- package/dist/bot/kit.js.map +1 -0
- package/dist/bot/llm-stream.d.ts +84 -0
- package/dist/bot/llm-stream.d.ts.map +1 -0
- package/dist/bot/llm-stream.js +201 -0
- package/dist/bot/llm-stream.js.map +1 -0
- package/package.json +51 -2
package/README.md
CHANGED
|
@@ -200,6 +200,7 @@ Every `SourcedError` carries `source`, `operation`, `status`, `context`, and the
|
|
|
200
200
|
- **XDG**: XDG Base Directory paths — reads env vars set by [xdg-dirs](https://github.com/adriangalilea/xdg-dirs), falls back to spec defaults
|
|
201
201
|
- **Unseen**: Persistent dedup filter — "what's new since last time?" for cron/monitoring workflows
|
|
202
202
|
- **Project Discovery**: Find project/monorepo roots, detect JS/TS projects
|
|
203
|
+
- **Bot plugins (GramIO)**: `kit` (graceful shutdown + admin context), `access-control` (gate + approve/deny menu, backed by sessions), `llm-stream` (streaming LLM markdown to Telegram with graceful degradation)
|
|
203
204
|
|
|
204
205
|
### XDG Base Directories
|
|
205
206
|
|
|
@@ -249,6 +250,51 @@ newMessages = [{ id: '2', from: 'bob', text: 'hey' }]
|
|
|
249
250
|
|
|
250
251
|
Saves state to: `$XDG_STATE_HOME/unseen/{name}.json`
|
|
251
252
|
|
|
253
|
+
### Telegram bot plugins (GramIO)
|
|
254
|
+
|
|
255
|
+
Plugins for personal Telegram bots built on [GramIO](https://gramio.dev). Each plugin lives at its own subpath; peer deps (`gramio`, `@gramio/storage`, `@gramio/session`, `@gramio/format`, `marked`) are **all optional** — install only what you import.
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
pnpm add @adriangalilea/utils gramio @gramio/storage @gramio/session
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
| Subpath | What it does |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `@adriangalilea/utils/bot/kit` | `gracefulStart(bot)` — SIGINT/SIGTERM → `bot.stop()` → exit; force-kills if shutdown hangs.<br>`adminContext({ adminId? })` — reads `TELEGRAM_ADMIN_ID` from `kev` (with optional hardcoded fallback), decorates `ctx.adminId` + `ctx.isAdmin`. |
|
|
264
|
+
| `@adriangalilea/utils/bot/access-control` | Personal-bot ACL — gates non-admin/non-default users; admin gets DM with `[✅ Aprobar][❌ Denegar]` on first attempt; `/access` opens a persistent menu (revoke / reapprove / list pending). Backed by `@gramio/session` per-user + a small index. |
|
|
265
|
+
| `@adriangalilea/utils/bot/coalesce` | Joins client-split inbound messages back into one. When a user pastes >4096 chars, Telegram clients fragment it into separate `message` updates with no marker. Middleware detects the burst and emits one combined event. |
|
|
266
|
+
| `@adriangalilea/utils/bot/llm-stream` | `ctx.startStream()` for LLM token streams. Debounced `editMessageText`, splits at 4000 chars on paragraph/line/word boundary, parses Markdown locally so malformed mid-stream markup degrades to plain text instead of failing. |
|
|
267
|
+
|
|
268
|
+
Standard wiring:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { Bot } from 'gramio'
|
|
272
|
+
import { redisStorage } from '@gramio/storage-redis'
|
|
273
|
+
import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
274
|
+
import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
275
|
+
import { llmStream } from '@adriangalilea/utils/bot/llm-stream'
|
|
276
|
+
|
|
277
|
+
const storage = redisStorage() // ONE instance, shared
|
|
278
|
+
|
|
279
|
+
const bot = new Bot(process.env.BOT_TOKEN!)
|
|
280
|
+
.extend(adminContext({ adminId: 190202471 })) // KEV.TELEGRAM_ADMIN_ID overrides
|
|
281
|
+
.extend(accessControl({ storage, defaults: [] })) // gate; depends on adminContext
|
|
282
|
+
.extend(llmStream())
|
|
283
|
+
.command('chat', async (ctx) => {
|
|
284
|
+
const stream = ctx.startStream()
|
|
285
|
+
for await (const chunk of yourLLM()) await stream.append(chunk.text)
|
|
286
|
+
await stream.end()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
await gracefulStart(bot)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Inside handlers, `ctx.access` is a typed discriminated union — `{ allowed: true, source: 'admin' | 'default' | 'store', record? }` or `{ allowed: false, reason }`. `ctx.adminId` and `ctx.isAdmin` are available on every event from `adminContext`.
|
|
293
|
+
|
|
294
|
+
For tests/demos without a second Telegram account, `simulateAccessRequest(bot, storage, adminId, fakeUser, msg)` injects a synthetic pending request so admin can exercise the approve/deny flow.
|
|
295
|
+
|
|
296
|
+
See `src/bot/CLAUDE.md` for storage layout, design decisions, and gotchas.
|
|
297
|
+
|
|
252
298
|
## Release
|
|
253
299
|
|
|
254
300
|
Bump version in `package.json` (and `jsr.json`), push to `main`. CI handles everything:
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access control for personal GramIO bots — a one-stop guard +
|
|
3
|
+
* approve/deny + revocable allow-list with an inline admin menu.
|
|
4
|
+
*
|
|
5
|
+
* stranger DMs your bot
|
|
6
|
+
* │
|
|
7
|
+
* ▼
|
|
8
|
+
* ┌──── plugin gate (this file) ────────────────────┐
|
|
9
|
+
* │ ctx.from.id ∈ admin / defaults / approved? │
|
|
10
|
+
* │ yes → next() │
|
|
11
|
+
* │ no → drop + notify admin (rate-limited) │
|
|
12
|
+
* └─────────────────────────────────────────────────┘
|
|
13
|
+
* │
|
|
14
|
+
* admin gets DM with [✅ Aprobar] [❌ Denegar]
|
|
15
|
+
* │
|
|
16
|
+
* admin taps
|
|
17
|
+
* │
|
|
18
|
+
* stranger's session updated · stranger gets DM
|
|
19
|
+
*
|
|
20
|
+
* **Storage layout.** Per-user state lives in its own key, written
|
|
21
|
+
* through `@gramio/session` so the gate read on the hot path costs
|
|
22
|
+
* nothing extra (session is loaded for the user already). A single
|
|
23
|
+
* tiny index key keeps track of who's pending / approved / denied so
|
|
24
|
+
* the `/access` admin menu can list without scanning the whole DB.
|
|
25
|
+
*
|
|
26
|
+
* storage:
|
|
27
|
+
* access:<userId> → AccessRecord (the user's session)
|
|
28
|
+
* ac:index → { pending, approved, denied }
|
|
29
|
+
*
|
|
30
|
+
* **Cross-user mutations.** When you tap `[✅ Aprobar]` on Pepe's
|
|
31
|
+
* notification, ctx is *yours* (the admin), so `ctx.access` is your
|
|
32
|
+
* own record. To mutate Pepe's record we hit the storage at the same
|
|
33
|
+
* key format we registered the session with (`access:<id>`) and
|
|
34
|
+
* update the index. This isn't a hack — it's our own module
|
|
35
|
+
* coordinating with itself.
|
|
36
|
+
*
|
|
37
|
+
* **Composes with `adminContext`** (kit.ts) — that plugin must be
|
|
38
|
+
* extended first or `bot.start()` throws. Inside this plugin,
|
|
39
|
+
* `ctx.adminId` and `ctx.isAdmin` are typed.
|
|
40
|
+
*
|
|
41
|
+
* Peer deps: `gramio`, `@gramio/storage`, `@gramio/session`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { Bot } from 'gramio'
|
|
45
|
+
* import { redisStorage } from '@gramio/storage-redis'
|
|
46
|
+
* import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
47
|
+
* import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
48
|
+
*
|
|
49
|
+
* const storage = redisStorage()
|
|
50
|
+
*
|
|
51
|
+
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
52
|
+
* .extend(adminContext({ adminId: 190202471 }))
|
|
53
|
+
* .extend(accessControl({ storage, defaults: [1158734055] }))
|
|
54
|
+
* .command('start', (ctx) => ctx.send(`hola, source=${ctx.access.source}`))
|
|
55
|
+
*
|
|
56
|
+
* await gracefulStart(bot)
|
|
57
|
+
*/
|
|
58
|
+
import { type AnyBot, type DeriveDefinitions, Plugin } from 'gramio';
|
|
59
|
+
import { type Storage } from '@gramio/storage';
|
|
60
|
+
export type AccessStatus = 'unknown' | 'pending' | 'approved' | 'denied';
|
|
61
|
+
export type AccessUser = {
|
|
62
|
+
id: number;
|
|
63
|
+
firstName?: string;
|
|
64
|
+
lastName?: string;
|
|
65
|
+
username?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The shape persisted per-user via session at `access:<userId>`.
|
|
69
|
+
* `unknown` is the initial state (session never seen this user).
|
|
70
|
+
*/
|
|
71
|
+
export type AccessRecord = {
|
|
72
|
+
status: AccessStatus;
|
|
73
|
+
user?: AccessUser;
|
|
74
|
+
/** Chat to DM the user back. For private chats this equals user.id. */
|
|
75
|
+
chatId?: number;
|
|
76
|
+
requestedAt?: number;
|
|
77
|
+
approvedAt?: number;
|
|
78
|
+
approvedBy?: number;
|
|
79
|
+
deniedAt?: number;
|
|
80
|
+
deniedBy?: number;
|
|
81
|
+
/** First message text from the request (truncated). */
|
|
82
|
+
firstMessage?: string;
|
|
83
|
+
lastActivityAt?: number;
|
|
84
|
+
messageCount?: number;
|
|
85
|
+
/** Counts attempts after the initial request — used by the throttle. */
|
|
86
|
+
rejectedAttempts?: number;
|
|
87
|
+
lastNotifiedAt?: number;
|
|
88
|
+
};
|
|
89
|
+
export type AccessIndex = {
|
|
90
|
+
pending: number[];
|
|
91
|
+
approved: number[];
|
|
92
|
+
denied: number[];
|
|
93
|
+
};
|
|
94
|
+
export type AccessSource = 'admin' | 'default' | 'store';
|
|
95
|
+
/**
|
|
96
|
+
* What handlers downstream see on `ctx.access`. A discriminated union —
|
|
97
|
+
* use the `allowed` field to narrow.
|
|
98
|
+
*/
|
|
99
|
+
export type AccessInfo = {
|
|
100
|
+
allowed: true;
|
|
101
|
+
source: AccessSource;
|
|
102
|
+
/** The persisted record, when source is 'store'. */
|
|
103
|
+
record?: AccessRecord;
|
|
104
|
+
} | {
|
|
105
|
+
allowed: false;
|
|
106
|
+
reason: 'denied' | 'pending' | 'unknown' | 'no-sender';
|
|
107
|
+
};
|
|
108
|
+
export type AccessControlOptions = {
|
|
109
|
+
/** Persistence. Default `inMemoryStorage()` (data lost on restart — warns once). */
|
|
110
|
+
storage?: Storage;
|
|
111
|
+
/** Always-allowed user ids, hardcoded. Bypass the entire flow. */
|
|
112
|
+
defaults?: ReadonlyArray<number>;
|
|
113
|
+
/** Reply sent to denied users on first attempt. `false` to silence. */
|
|
114
|
+
denyMessage?: string | false;
|
|
115
|
+
/** Min ms between repeat admin notifications for the same user. Default 6h. */
|
|
116
|
+
notifyThrottleMs?: number;
|
|
117
|
+
/** Callbacks for your own logging / metrics. */
|
|
118
|
+
onAccessRequest?: (info: {
|
|
119
|
+
user: AccessUser;
|
|
120
|
+
firstMessage?: string;
|
|
121
|
+
}) => void;
|
|
122
|
+
onApprove?: (info: {
|
|
123
|
+
userId: number;
|
|
124
|
+
approvedBy: number;
|
|
125
|
+
}) => void;
|
|
126
|
+
onDeny?: (info: {
|
|
127
|
+
userId: number;
|
|
128
|
+
deniedBy: number;
|
|
129
|
+
}) => void;
|
|
130
|
+
};
|
|
131
|
+
type AdminDerives = {
|
|
132
|
+
adminId: number;
|
|
133
|
+
isAdmin: boolean;
|
|
134
|
+
};
|
|
135
|
+
type AccessSessionDerives = {
|
|
136
|
+
_accessSession: AccessRecord;
|
|
137
|
+
};
|
|
138
|
+
type AccessDerives = {
|
|
139
|
+
access: AccessInfo;
|
|
140
|
+
};
|
|
141
|
+
export declare const accessControl: (opts?: AccessControlOptions) => Plugin<{}, DeriveDefinitions & {
|
|
142
|
+
global: AdminDerives & AccessSessionDerives & AccessDerives;
|
|
143
|
+
} & {
|
|
144
|
+
message: {
|
|
145
|
+
_accessSession: AccessRecord & {
|
|
146
|
+
$clear: () => Promise<void>;
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
channel_post: {
|
|
150
|
+
_accessSession: AccessRecord & {
|
|
151
|
+
$clear: () => Promise<void>;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
inline_query: {
|
|
155
|
+
_accessSession: AccessRecord & {
|
|
156
|
+
$clear: () => Promise<void>;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
chosen_inline_result: {
|
|
160
|
+
_accessSession: AccessRecord & {
|
|
161
|
+
$clear: () => Promise<void>;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
callback_query: {
|
|
165
|
+
_accessSession: AccessRecord & {
|
|
166
|
+
$clear: () => Promise<void>;
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
shipping_query: {
|
|
170
|
+
_accessSession: AccessRecord & {
|
|
171
|
+
$clear: () => Promise<void>;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
pre_checkout_query: {
|
|
175
|
+
_accessSession: AccessRecord & {
|
|
176
|
+
$clear: () => Promise<void>;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
poll_answer: {
|
|
180
|
+
_accessSession: AccessRecord & {
|
|
181
|
+
$clear: () => Promise<void>;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
chat_join_request: {
|
|
185
|
+
_accessSession: AccessRecord & {
|
|
186
|
+
$clear: () => Promise<void>;
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
new_chat_members: {
|
|
190
|
+
_accessSession: AccessRecord & {
|
|
191
|
+
$clear: () => Promise<void>;
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
new_chat_title: {
|
|
195
|
+
_accessSession: AccessRecord & {
|
|
196
|
+
$clear: () => Promise<void>;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
new_chat_photo: {
|
|
200
|
+
_accessSession: AccessRecord & {
|
|
201
|
+
$clear: () => Promise<void>;
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
delete_chat_photo: {
|
|
205
|
+
_accessSession: AccessRecord & {
|
|
206
|
+
$clear: () => Promise<void>;
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
group_chat_created: {
|
|
210
|
+
_accessSession: AccessRecord & {
|
|
211
|
+
$clear: () => Promise<void>;
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
message_auto_delete_timer_changed: {
|
|
215
|
+
_accessSession: AccessRecord & {
|
|
216
|
+
$clear: () => Promise<void>;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
migrate_to_chat_id: {
|
|
220
|
+
_accessSession: AccessRecord & {
|
|
221
|
+
$clear: () => Promise<void>;
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
migrate_from_chat_id: {
|
|
225
|
+
_accessSession: AccessRecord & {
|
|
226
|
+
$clear: () => Promise<void>;
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
pinned_message: {
|
|
230
|
+
_accessSession: AccessRecord & {
|
|
231
|
+
$clear: () => Promise<void>;
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
invoice: {
|
|
235
|
+
_accessSession: AccessRecord & {
|
|
236
|
+
$clear: () => Promise<void>;
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
successful_payment: {
|
|
240
|
+
_accessSession: AccessRecord & {
|
|
241
|
+
$clear: () => Promise<void>;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
chat_shared: {
|
|
245
|
+
_accessSession: AccessRecord & {
|
|
246
|
+
$clear: () => Promise<void>;
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
proximity_alert_triggered: {
|
|
250
|
+
_accessSession: AccessRecord & {
|
|
251
|
+
$clear: () => Promise<void>;
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
video_chat_scheduled: {
|
|
255
|
+
_accessSession: AccessRecord & {
|
|
256
|
+
$clear: () => Promise<void>;
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
video_chat_started: {
|
|
260
|
+
_accessSession: AccessRecord & {
|
|
261
|
+
$clear: () => Promise<void>;
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
video_chat_ended: {
|
|
265
|
+
_accessSession: AccessRecord & {
|
|
266
|
+
$clear: () => Promise<void>;
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
video_chat_participants_invited: {
|
|
270
|
+
_accessSession: AccessRecord & {
|
|
271
|
+
$clear: () => Promise<void>;
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
web_app_data: {
|
|
275
|
+
_accessSession: AccessRecord & {
|
|
276
|
+
$clear: () => Promise<void>;
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
location: {
|
|
280
|
+
_accessSession: AccessRecord & {
|
|
281
|
+
$clear: () => Promise<void>;
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
passport_data: {
|
|
285
|
+
_accessSession: AccessRecord & {
|
|
286
|
+
$clear: () => Promise<void>;
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
} & {
|
|
290
|
+
global: {
|
|
291
|
+
access: {
|
|
292
|
+
allowed: false;
|
|
293
|
+
reason: "no-sender";
|
|
294
|
+
source?: undefined;
|
|
295
|
+
record?: undefined;
|
|
296
|
+
};
|
|
297
|
+
} | {
|
|
298
|
+
access: {
|
|
299
|
+
allowed: true;
|
|
300
|
+
source: "admin";
|
|
301
|
+
reason?: undefined;
|
|
302
|
+
record?: undefined;
|
|
303
|
+
};
|
|
304
|
+
} | {
|
|
305
|
+
access: {
|
|
306
|
+
allowed: true;
|
|
307
|
+
source: "default";
|
|
308
|
+
reason?: undefined;
|
|
309
|
+
record?: undefined;
|
|
310
|
+
};
|
|
311
|
+
} | {
|
|
312
|
+
access: {
|
|
313
|
+
allowed: true;
|
|
314
|
+
source: "store";
|
|
315
|
+
record: AccessRecord;
|
|
316
|
+
reason?: undefined;
|
|
317
|
+
};
|
|
318
|
+
} | {
|
|
319
|
+
access: {
|
|
320
|
+
allowed: false;
|
|
321
|
+
reason: "unknown" | "pending" | "denied";
|
|
322
|
+
source?: undefined;
|
|
323
|
+
record?: undefined;
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
}, {}>;
|
|
327
|
+
/**
|
|
328
|
+
* Inject a synthetic access request — for tests/demos when you can't
|
|
329
|
+
* easily spin up a second Telegram account. Writes a `pending` record
|
|
330
|
+
* to storage at the same key the plugin's session would, updates the
|
|
331
|
+
* index, then DMs the admin with the real
|
|
332
|
+
* `[✅ Aprobar][❌ Denegar]` keyboard. Tapping those buttons exercises
|
|
333
|
+
* the real callback handlers end-to-end.
|
|
334
|
+
*
|
|
335
|
+
* Pass the SAME `storage` instance you passed to `accessControl({ storage })`.
|
|
336
|
+
*/
|
|
337
|
+
export declare const simulateAccessRequest: (bot: AnyBot, storage: Storage, adminId: number, fakeUser: AccessUser, message?: string) => Promise<void>;
|
|
338
|
+
export {};
|
|
339
|
+
//# sourceMappingURL=access-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/bot/access-control.ts"],"names":[],"mappings":"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"}
|