@amykit/plugin-telegram 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,75 @@
1
- # Temporary Holding Version
1
+ <!-- Generated by `npm run docs:generate` from the package itself. Do not edit by hand. -->
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @amykit/plugin-telegram
4
+
5
+ A topic per piece of work in the operator's private chat with a Telegram bot, as the conversation port.
6
+
7
+ A plugin for [amy](https://github.com/nicolasmelo1/amy). It provides the `conversation` port.
8
+
9
+ Use it when you want to follow and answer amy from Telegram. It mounts the conversation port: each piece of work gets a topic in your private chat with a bot, and it can also hold amy's chat. It needs the bot token in TELEGRAM_BOT_TOKEN and your numeric Telegram user id.
10
+
11
+ ## What it extends
12
+
13
+ | | |
14
+ | :-- | :-- |
15
+ | Ports | `conversation` |
16
+ | Actions | _none_ |
17
+ | Operations | _none_ |
18
+ | Commands | _none_ |
19
+ | Events | _none_ |
20
+ | Environment | `TELEGRAM_BOT_TOKEN` |
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ amy add @amykit/plugin-telegram
26
+ ```
27
+
28
+ Then name it in `~/.amy/config.yaml`:
29
+
30
+ ```yaml
31
+ workflows:
32
+ your-profile:
33
+ plugins:
34
+ - "@amykit/plugin-telegram"
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ ```yaml
40
+ plugins:
41
+ "@amykit/plugin-telegram":
42
+ directory: "telegram"
43
+ operator: <string>
44
+ server: "https://api.telegram.org"
45
+ topics: true
46
+ ```
47
+
48
+ | Setting | Type | Required | Default | What it is |
49
+ | :-- | :-- | :-- | :-- | :-- |
50
+ | `directory` | `string` | no | `telegram` | where this profile's topics are remembered, by work id, relative to the state directory; the replies read for every profile are kept once per bot |
51
+ | `operator` | `string` | **yes** | | your Telegram user id, a number such as 123456789: the private chat each work item's topic opens in, and the only person whose replies are answers |
52
+ | `server` | `string` | no | `https://api.telegram.org` | the Bot API server, for a local one instead of Telegram's |
53
+ | `topics` | `boolean` | no | `true` | whether each piece of work opens a topic of its own; off, works share the private chat and a reply reaches one by quoting amy's message for it or starting with its id |
54
+
55
+ Every field is checked at boot against the schema this package declares. A key that is
56
+ not one of the above is a refusal naming the plugin and the key, not a setting that
57
+ silently never applied.
58
+
59
+ ## Environment
60
+
61
+ This package reads the following, which `amy doctor` checks for you:
62
+
63
+ | Variable | |
64
+ | :-- | :-- |
65
+ | `TELEGRAM_BOT_TOKEN` | required at mount |
66
+
67
+ ## Where the reasoning is
68
+
69
+ - [Writing a plugin](https://github.com/nicolasmelo1/amy/tree/main/docs/guides/write-a-plugin.md)
70
+ - [Reference: plugins](https://github.com/nicolasmelo1/amy/tree/main/docs/reference/plugins.md)
71
+ - [Ports and the registry](https://github.com/nicolasmelo1/amy/tree/main/docs/concepts/ports.md)
72
+
73
+ ## Licence
74
+
75
+ MIT
@@ -0,0 +1,74 @@
1
+ /** One HTTP exchange, small enough to script in a test. */
2
+ export interface HttpRequest {
3
+ url: string;
4
+ method: "GET" | "POST";
5
+ headers: Record<string, string>;
6
+ body?: string;
7
+ /** Refuse the answer once its body passes this many bytes, while it arrives. */
8
+ maxBytes?: number;
9
+ /** Abandon the exchange, the body's last byte included, after this long. */
10
+ deadlineMs?: number;
11
+ }
12
+ export interface HttpResponse {
13
+ status: number;
14
+ body: Uint8Array;
15
+ }
16
+ export type HttpTransport = (request: HttpRequest) => Promise<HttpResponse>;
17
+ /** An answer larger than the request allowed, refused before it was held whole. */
18
+ export declare class TooLarge extends Error {
19
+ readonly limit: number;
20
+ constructor(limit: number);
21
+ }
22
+ export declare const fetchTransport: HttpTransport;
23
+ /** Telegram's own server; a local Bot API server, or a stand-in, is another. */
24
+ export declare const TELEGRAM_API = "https://api.telegram.org";
25
+ /** How many times a `429` is waited out before the call fails. */
26
+ export declare const DEFAULT_RATE_LIMIT_RETRIES = 5;
27
+ /**
28
+ * How long one exchange may take, body included. Updates are asked for with
29
+ * no long poll, so anything this slow is a server that stopped answering, and
30
+ * holding the bot's lock for it would stall every profile.
31
+ */
32
+ export declare const DEFAULT_DEADLINE_MS = 60000;
33
+ /**
34
+ * What the Bot API answered when it refused, with its `error_code` kept
35
+ * apart, so a 409 is told from a 400 without reading the words.
36
+ */
37
+ export declare class BotApiRefusal extends Error {
38
+ readonly method: string;
39
+ readonly code: number;
40
+ readonly description: string;
41
+ constructor(method: string, code: number, description: string);
42
+ }
43
+ export interface BotApiOptions {
44
+ server?: string;
45
+ transport?: HttpTransport;
46
+ sleep?: (ms: number) => Promise<void>;
47
+ maxRateLimitRetries?: number;
48
+ deadlineMs?: number;
49
+ }
50
+ /**
51
+ * The Bot API, called by method name with a JSON body.
52
+ *
53
+ * The token is part of every URL Telegram serves, so no URL is ever put in
54
+ * an error, a log or a file: what goes wrong is said by method name.
55
+ */
56
+ export declare class BotApi {
57
+ private readonly token;
58
+ private readonly server;
59
+ private readonly transport;
60
+ private readonly sleep;
61
+ private readonly maxRetries;
62
+ private readonly deadlineMs;
63
+ constructor(token: string, options?: BotApiOptions);
64
+ /** The method's `result`, or a {@link BotApiRefusal} when `ok` is false. */
65
+ call<T>(method: string, args?: Record<string, unknown>): Promise<T>;
66
+ /** A file's bytes, by the `file_path` getFile answered, refused past `maxBytes` while they arrive. */
67
+ download(filePath: string, maxBytes?: number): Promise<Uint8Array>;
68
+ /**
69
+ * The transport's failure, said without the URL that carries the token. The
70
+ * original is not kept as the cause: its message may carry that URL.
71
+ */
72
+ private send;
73
+ }
74
+ //# sourceMappingURL=BotApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BotApi.d.ts","sourceRoot":"","sources":["../src/BotApi.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAE5E,mFAAmF;AACnF,qBAAa,QAAS,SAAQ,KAAK;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAGnC;AAED,eAAO,MAAM,cAAc,EAAE,aAK5B,CAAC;AAkCF,gFAAgF;AAChF,eAAO,MAAM,YAAY,6BAA6B,CAAC;AAEvD,kEAAkE;AAClE,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C;;;GAGG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAEpC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM;gBAFnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM;CAI/B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAUD;;;;;GAKG;AACH,qBAAa,MAAM;IAQf,OAAO,CAAC,QAAQ,CAAC,KAAK;IAPxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAGjB,KAAK,EAAE,MAAM,EAC9B,OAAO,GAAE,aAAkB;IAS7B,4EAA4E;IACtE,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAsB7E,sGAAsG;IAChG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAcxE;;;OAGG;IACH,OAAO,CAAC,IAAI;CAMb"}
package/dist/BotApi.js ADDED
@@ -0,0 +1,162 @@
1
+ /** An answer larger than the request allowed, refused before it was held whole. */
2
+ export class TooLarge extends Error {
3
+ limit;
4
+ constructor(limit) {
5
+ super(`the answer is larger than ${limit} bytes`);
6
+ this.limit = limit;
7
+ }
8
+ }
9
+ export const fetchTransport = async (request) => {
10
+ // One signal for the request and the body, so a server that trickles bytes forever is cut off too.
11
+ const signal = request.deadlineMs === undefined ? undefined : AbortSignal.timeout(request.deadlineMs);
12
+ const response = await fetch(request.url, { method: request.method, headers: request.headers, body: request.body, ...(signal ? { signal } : {}) });
13
+ return { status: response.status, body: await readBody(response, request.maxBytes) };
14
+ };
15
+ /**
16
+ * The body, counted as it arrives: a declared length over the limit is
17
+ * refused unread, and a missing or understated one is caught by the count.
18
+ */
19
+ async function readBody(response, maxBytes) {
20
+ if (maxBytes === undefined || !response.body)
21
+ return new Uint8Array(await response.arrayBuffer());
22
+ if (Number(response.headers.get("content-length") ?? 0) > maxBytes) {
23
+ await response.body.cancel();
24
+ throw new TooLarge(maxBytes);
25
+ }
26
+ const chunks = [];
27
+ let total = 0;
28
+ const reader = response.body.getReader();
29
+ for (;;) {
30
+ const { done, value } = await reader.read();
31
+ if (done)
32
+ break;
33
+ total += value.byteLength;
34
+ if (total > maxBytes) {
35
+ await reader.cancel();
36
+ throw new TooLarge(maxBytes);
37
+ }
38
+ chunks.push(value);
39
+ }
40
+ const body = new Uint8Array(total);
41
+ let offset = 0;
42
+ for (const chunk of chunks) {
43
+ body.set(chunk, offset);
44
+ offset += chunk.byteLength;
45
+ }
46
+ return body;
47
+ }
48
+ /** Telegram's own server; a local Bot API server, or a stand-in, is another. */
49
+ export const TELEGRAM_API = "https://api.telegram.org";
50
+ /** How many times a `429` is waited out before the call fails. */
51
+ export const DEFAULT_RATE_LIMIT_RETRIES = 5;
52
+ /**
53
+ * How long one exchange may take, body included. Updates are asked for with
54
+ * no long poll, so anything this slow is a server that stopped answering, and
55
+ * holding the bot's lock for it would stall every profile.
56
+ */
57
+ export const DEFAULT_DEADLINE_MS = 60_000;
58
+ /**
59
+ * What the Bot API answered when it refused, with its `error_code` kept
60
+ * apart, so a 409 is told from a 400 without reading the words.
61
+ */
62
+ export class BotApiRefusal extends Error {
63
+ method;
64
+ code;
65
+ description;
66
+ constructor(method, code, description) {
67
+ super(`telegram ${method} refused: ${code} ${description}`);
68
+ this.method = method;
69
+ this.code = code;
70
+ this.description = description;
71
+ }
72
+ }
73
+ /**
74
+ * The Bot API, called by method name with a JSON body.
75
+ *
76
+ * The token is part of every URL Telegram serves, so no URL is ever put in
77
+ * an error, a log or a file: what goes wrong is said by method name.
78
+ */
79
+ export class BotApi {
80
+ token;
81
+ server;
82
+ transport;
83
+ sleep;
84
+ maxRetries;
85
+ deadlineMs;
86
+ constructor(token, options = {}) {
87
+ this.token = token;
88
+ this.server = (options.server ?? TELEGRAM_API).replace(/\/+$/, "");
89
+ this.transport = options.transport ?? fetchTransport;
90
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
91
+ this.maxRetries = options.maxRateLimitRetries ?? DEFAULT_RATE_LIMIT_RETRIES;
92
+ this.deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;
93
+ }
94
+ /** The method's `result`, or a {@link BotApiRefusal} when `ok` is false. */
95
+ async call(method, args = {}) {
96
+ for (let attempt = 0;; attempt++) {
97
+ const response = await this.send(method, {
98
+ url: `${this.server}/bot${this.token}/${method}`,
99
+ method: "POST",
100
+ headers: { "Content-Type": "application/json" },
101
+ body: JSON.stringify(args),
102
+ deadlineMs: this.deadlineMs,
103
+ });
104
+ const answer = parse(method, response);
105
+ if (answer.ok === true)
106
+ return answer.result;
107
+ const code = answer.error_code ?? response.status;
108
+ const retryAfter = answer.parameters?.retry_after;
109
+ if (code === 429 && attempt < this.maxRetries) {
110
+ await this.sleep(Math.max(0, retryAfter ?? 1) * 1000);
111
+ continue;
112
+ }
113
+ // A local server or a proxy may echo the request, and with it the URL.
114
+ throw new BotApiRefusal(method, code, withoutToken(answer.description ?? "no description", this.token));
115
+ }
116
+ }
117
+ /** A file's bytes, by the `file_path` getFile answered, refused past `maxBytes` while they arrive. */
118
+ async download(filePath, maxBytes) {
119
+ const response = await this.send("file download", {
120
+ url: `${this.server}/file/bot${this.token}/${filePath}`,
121
+ method: "GET",
122
+ headers: {},
123
+ deadlineMs: this.deadlineMs,
124
+ ...(maxBytes === undefined ? {} : { maxBytes }),
125
+ });
126
+ if (response.status !== 200)
127
+ throw new Error(`telegram file download answered HTTP ${response.status}`);
128
+ // Checked again here for a transport that does not honour the limit.
129
+ if (maxBytes !== undefined && response.body.byteLength > maxBytes)
130
+ throw new TooLarge(maxBytes);
131
+ return response.body;
132
+ }
133
+ /**
134
+ * The transport's failure, said without the URL that carries the token. The
135
+ * original is not kept as the cause: its message may carry that URL.
136
+ */
137
+ send(what, request) {
138
+ return this.transport(request).catch((error) => {
139
+ if (error instanceof TooLarge)
140
+ throw error;
141
+ throw new Error(`telegram ${what} could not be reached: ${withoutToken(error, this.token)}`);
142
+ });
143
+ }
144
+ }
145
+ function parse(method, response) {
146
+ try {
147
+ return JSON.parse(new TextDecoder().decode(response.body));
148
+ }
149
+ catch {
150
+ throw new Error(`telegram ${method} answered HTTP ${response.status} with a body that is not JSON`);
151
+ }
152
+ }
153
+ /** The message with the token taken out, as written and as a URL would encode it, in either case. */
154
+ function withoutToken(error, token) {
155
+ const text = error instanceof Error ? error.message : String(error);
156
+ if (!token)
157
+ return text;
158
+ const encoded = encodeURIComponent(token);
159
+ const forms = [...new Set([token, encoded, encoded.replace(/%[0-9A-F]{2}/g, (escape) => escape.toLowerCase())])];
160
+ return forms.reduce((said, form) => said.split(form).join("<token>"), text);
161
+ }
162
+ //# sourceMappingURL=BotApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BotApi.js","sourceRoot":"","sources":["../src/BotApi.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,207 @@
1
+ import { Ask, ChatMessage, Conversation, Reply, ThreadRef } from "@amykit/core";
2
+ import { BotApi } from "./BotApi.js";
3
+ export interface TelegramConversationOptions {
4
+ /** The operator's user id, which is also their private chat with the bot. */
5
+ operator: string;
6
+ /**
7
+ * What this profile owns: its topics by work id, the replies filed under
8
+ * them and the files they carried. Removing the profile removes all of it.
9
+ */
10
+ directory: string;
11
+ /**
12
+ * What one bot shares across profiles: which profile owns each topic, the
13
+ * update offset and the lock. The directory itself when not given.
14
+ */
15
+ shared?: string;
16
+ /** The largest file downloaded; a larger one is named in the reply instead. */
17
+ maxFileBytes?: number;
18
+ /** How long to wait for another process reading the bot. */
19
+ lockWaitMs?: number;
20
+ /** How long a hold on the bot lasts without being renewed. */
21
+ leaseMs?: number;
22
+ /** An inspection may read existing state but must not initialize it. */
23
+ readOnly?: boolean;
24
+ /**
25
+ * Whether each piece of work opens a topic of its own. Off, works share the
26
+ * private chat, and a reply reaches one only when it quotes amy's message
27
+ * for it or starts with its id (S15).
28
+ */
29
+ topics?: boolean;
30
+ }
31
+ /** What `amy doctor` prints for this plugin, one line per question. */
32
+ export interface TelegramCheck {
33
+ label: string;
34
+ ok: boolean;
35
+ detail?: string;
36
+ }
37
+ /** The most a bot may download, by the Bot API's own limit. */
38
+ export declare const DEFAULT_MAX_FILE_BYTES: number;
39
+ /** A posted structured ask and its durable selection/acceptance state. */
40
+ export interface FiledAsk {
41
+ ask: Ask;
42
+ messageId: number;
43
+ at: string;
44
+ selected: number[];
45
+ accepted?: {
46
+ at: string;
47
+ chosen: string[];
48
+ };
49
+ /** Callback ids already applied; retries must not toggle a persisted selection. */
50
+ callbacks?: string[];
51
+ acknowledged?: boolean;
52
+ }
53
+ /**
54
+ * One topic per piece of work, in the operator's private chat with the bot.
55
+ *
56
+ * Telegram hands a bot its updates once, to whoever asks first, so the
57
+ * process reading them files each message, durably, under the profile that
58
+ * owns its topic before the offset that confirms it moves.
59
+ *
60
+ * Nothing here depends on that reader being alone. Every reply is a file of
61
+ * its own, created and never rewritten; every topic and owner is a file
62
+ * created once; the offset only moves forward. A second reader, or one that
63
+ * was paused past its hold, can only add what is already there, never lose
64
+ * it. The hold on the bot saves work (a 409 from Telegram, a topic opened
65
+ * twice) and nothing else.
66
+ */
67
+ export declare class TelegramConversation implements Conversation {
68
+ private readonly api;
69
+ private readonly options;
70
+ readonly asks: true;
71
+ readonly edit: true;
72
+ readonly threads?: true;
73
+ /** The bot: its directory is named for it, so two bots never share a conversation. */
74
+ readonly source: string;
75
+ private readonly opening;
76
+ /** Who this profile's directory is: a directory removed and made again is somebody else. */
77
+ private readonly identity;
78
+ /** The operator's id as Telegram writes it, whatever spelling the config used. */
79
+ private readonly operator;
80
+ constructor(api: BotApi, options: TelegramConversationOptions);
81
+ open(workId: string, title: string): Promise<ThreadRef>;
82
+ /**
83
+ * A work in a chat without topics: nothing to ask Telegram for, only its
84
+ * thread to name and record, so a reply addressed to it is filed under it.
85
+ */
86
+ private openInTheChat;
87
+ /**
88
+ * Records who owns a work's thread, the thread by work id, and the work by
89
+ * its id for the whole bot, so a message addressed to it from outside every
90
+ * thread finds it. Owner first: a reply that arrives before the work knows
91
+ * its thread is still filed here. The directory is kept relative to the
92
+ * bot's, so a state root moved or mounted elsewhere still finds it.
93
+ */
94
+ private record;
95
+ post(thread: ThreadRef, message: {
96
+ text: string;
97
+ files?: string[];
98
+ ask?: Ask;
99
+ }): Promise<string>;
100
+ private postAsk;
101
+ private sendParts;
102
+ replies(thread: ThreadRef, since: string): Promise<Reply[]>;
103
+ /**
104
+ * Whether this bot can hold the conversation, asked of Telegram itself:
105
+ * the token, the bot's topic mode, a private chat the operator started,
106
+ * and nobody else reading its updates.
107
+ */
108
+ checks(): Promise<TelegramCheck[]>;
109
+ /**
110
+ * The messages in the private chat that reached no work, oldest first,
111
+ * until they are settled. Telegram is read for them first, so the chat is
112
+ * read all the time and not only when a work is looked at (S20).
113
+ */
114
+ chat(): Promise<ChatMessage[]>;
115
+ settle(id: string): Promise<void>;
116
+ /** Says something in the private chat, outside every topic, in as many messages as it takes. */
117
+ say(place: string, text: string): Promise<string>;
118
+ /** Replaces what a message amy said reads, as much of it as one message holds. */
119
+ revise(place: string, id: string, text: string): Promise<void>;
120
+ private chatId;
121
+ private topics;
122
+ /**
123
+ * Where each work's thread is remembered, one place per mode: a topic is no
124
+ * thread in a chat without topics, nor the other way round, so turning
125
+ * topics on or off opens each work afresh rather than reusing an id the
126
+ * new mode cannot post to.
127
+ */
128
+ private threadsDirectory;
129
+ /** Where a work's message goes: its topic, or the chat itself when works share it. */
130
+ private inThread;
131
+ private threadOf;
132
+ private workOfThread;
133
+ private shared;
134
+ private inbox;
135
+ /**
136
+ * Reads every update Telegram holds and files each of the operator's
137
+ * messages under the profile owning its topic. A topic no profile owns any
138
+ * more, its profile removed or made again, is skipped. The offset moves
139
+ * only once what it confirms is durably on disk.
140
+ */
141
+ private fileUpdates;
142
+ /** Handles a button only after resolving its real topic owner from the callback message. */
143
+ private fileCallback;
144
+ private callbackContext;
145
+ private applyCallback;
146
+ private sendChoices;
147
+ private choose;
148
+ private acknowledgeAccepted;
149
+ private redrawKeyboard;
150
+ private closeKeyboard;
151
+ private askFile;
152
+ private askByKey;
153
+ private writeAsk;
154
+ protected askedUnder(topic: string): FiledAsk[];
155
+ /**
156
+ * Files one message in its owner's inbox for that owner's identity, as a
157
+ * file of its own. The inbox is named for the identity, so a write racing a
158
+ * profile's removal lands where no later profile at that path reads.
159
+ */
160
+ private file;
161
+ /**
162
+ * Where a topic's replies are filed for its owner, if that owner's directory
163
+ * still carries the identity that opened the topic; made if needed.
164
+ */
165
+ private inboxOf;
166
+ /** The id of the last message amy posted in a topic by an instant, or 0 when none is known. */
167
+ private postedBy;
168
+ /** Files what arrived in a topic before its owner was recorded, now that it is. */
169
+ private adopt;
170
+ /** Whether a topic is being opened in this chat now, by any process: an opening older than a day was abandoned. */
171
+ private openingIn;
172
+ /** Replies kept for a topic whose opening was abandoned a day ago, and the openings themselves, go. */
173
+ private pruneAbandoned;
174
+ /**
175
+ * The offset to confirm with, unless it has outlived Telegram's update
176
+ * ids: then the earliest update Telegram still holds is asked for instead,
177
+ * and the ones already filed are filed once.
178
+ */
179
+ private offset;
180
+ /** Moves the offset forward, never back, so a late reader cannot unconfirm what another confirmed. */
181
+ private confirm;
182
+ /** The files as local paths, and a line naming each one too large to fetch. */
183
+ private downloadAll;
184
+ /**
185
+ * Fetches one file to its place, or answers false when it is too large: by
186
+ * what getFile says, by Telegram refusing it as too big when the message did
187
+ * not say its size, or by its bytes as they arrive. A local Bot API server
188
+ * answers with a path on its own disk, which is read where it lies.
189
+ */
190
+ private download;
191
+ private topicOfWork;
192
+ /**
193
+ * Files a message written outside every topic: under the work it is
194
+ * addressed to — a quote of amy's message for it, or its id at the start —
195
+ * and in the chat otherwise (S42). The rule is the core's; this only says
196
+ * what amy filed where (S16).
197
+ */
198
+ private fileOutside;
199
+ /**
200
+ * Runs `work` holding the bot, so one process reads its updates or opens a
201
+ * topic at a time: a hold not renewed within its lease is taken over. Being
202
+ * passed over costs a 409 or a second topic, never a reply, since nothing
203
+ * filed is ever rewritten.
204
+ */
205
+ private held;
206
+ }
207
+ //# sourceMappingURL=TelegramConversation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TelegramConversation.d.ts","sourceRoot":"","sources":["../src/TelegramConversation.ts"],"names":[],"mappings":"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"}