-
Notifications
You must be signed in to change notification settings - Fork 100
/
client.ts
279 lines (261 loc) · 9.26 KB
/
client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import {
ApiResponse,
debug as d,
Opts,
Telegram,
baseFetchConfig,
} from '../platform.ts'
import { GrammyError } from './error.ts'
import {
requiresFormDataUpload,
transformPayload,
createJsonPayload,
createFormDataPayload,
} from './payload.ts'
const debug = d('grammy:core')
// Available under `bot.api.raw`
/**
* Represents the raw Telegram Bot API with all methods specified 1:1 as
* documented on the website (https://core.telegram.org/bots/api).
*
* Every method takes an optional `AbortSignal` object that allows to cancel the
* API call if desired.
*/
export type RawApi = {
[M in keyof Telegram]: Parameters<Telegram[M]>[0] extends undefined
? (signal?: AbortSignal) => Promise<ReturnType<Telegram[M]>>
: (
args: Opts<M>,
signal?: AbortSignal
) => Promise<ReturnType<Telegram[M]>>
}
/**
* Small utility interface that abstracts from webhook reply calls of different
* web frameworks.
*/
export interface WebhookReplyEnvelope {
send?: (payload: string) => void | Promise<void>
}
/**
* Type of a function that can perform an API call. Used for Tranfromers.
*/
export type ApiCallFn = <M extends keyof RawApi>(
method: M,
payload: Opts<M>,
signal?: AbortSignal
) => Promise<ApiResponse<ReturnType<Telegram[M]>>>
/**
* API call transformers are functions that can access and modify the method and
* payload of an API call on the fly. This can be useful if you want to
* implement rate limiting or other things against the Telegram Bot API.
*
* Confer the grammY documentation to read more about how to use transformers.
*/
export type Transformer = <M extends keyof RawApi>(
prev: ApiCallFn,
method: M,
payload: Opts<M>,
signal?: AbortSignal
) => Promise<ApiResponse<ReturnType<Telegram[M]>>>
export type TransformerConsumer = TransformableApi['use']
/**
* A transformable API enhances the `RawApi` type by transformers.
*/
export interface TransformableApi {
/**
* Access to the raw API that the tranformers will be installed on.
*/
raw: RawApi
/**
* Can be used to register any number of transformers on the API.
*/
use: (...transformers: Transformer[]) => this
/**
* Returns a readonly list or the currently installed transformers. The list
* is sorted by time of installation where index 0 represents the
* transformer that was installed first.
*/
installedTransformers: Transformer[]
}
// Transformer base functions
const concatTransformer = (prev: ApiCallFn, trans: Transformer): ApiCallFn => (
method,
payload,
signal
) => trans(prev, method, payload, signal)
/**
* Options to pass to the API client that eventually connects to the Telegram
* Bot API server and makes the HTTP requests.
*/
export interface ApiClientOptions {
/**
* Root URL of the Telegram Bot API server. Default: https://api.telegram.org
*/
apiRoot?: string
/**
* URL builder function for API calls. Can be used to modify which API server
* should be called.
*
* @param root the root URL that was passed in `apiRoot`, or its default value
* @param token the bot's token that was passed when creating the bot
* @param method the API method to be called, e.g. `getMe`
* @return the url that will be fetched during the API call
*/
buildUrl?: (
root: string,
token: string,
method: string
) => Parameters<typeof fetch>[0]
/**
* If the bot is running on webhooks, as soon as the bot receives an update
* from Telegram, it is possible to make up to one API call in the response
* to the webhook request. As a benefit, this saves your bot from making up
* to one HTTP request per update. However, there are a number of drawbacks
* to using this:
* 1) You will not be able to handle potential errors of the respective API
* call.
* 2) More importantly, you also won't have access to the response object,
* so e.g. calling `sendMessage` will not give you access to the message
* you send.
* 3) Furthermore, it is not possible to cancel the request. The
* `AbortSignal` will be disregarded.
* 4) Note also that the types in grammY do not reflect the consequences of
* a performed webhook callback! For instance, they indicate that you
* always receive a response object, so it is your own responsibility to
* make sure you're not screwing up while using this minor performance
* optimization.
*
* With this warning out of the way, here is what you can do with the
* `canUseWebhookReply` option: it can be used to pass a function that
* determines whether to use webhook reply for the given method. It will
* only be invoked if the payload can be sent as JSON. It will not be
* invoked again for a given update after it returned `true`, indicating
* that the API call should be performed as a webhook send. In other words,
* subsequent API calls (during the same update) will always perform their
* own HTTP requests.
*
* @param method the method to call
*/
canUseWebhookReply?: (method: keyof RawApi) => boolean
/**
* Base configuration for `fetch` calls. Specify any additional parameters to
* use when fetching a method of the Telegram Bot API. Default: `{ compress:
* true }` (Node), `{}` (Deno)
*/
baseFetchConfig?: Omit<
Exclude<Parameters<typeof fetch>[1], undefined>,
'method' | 'headers' | 'body'
>
}
const DEFAULT_OPTIONS: Required<ApiClientOptions> = {
apiRoot: 'https://api.telegram.org',
buildUrl: (root, token, method) => `${root}/bot${token}/${method}`,
baseFetchConfig,
canUseWebhookReply: () => false,
}
class ApiClient {
private readonly options: Required<ApiClientOptions>
private hasUsedWebhookReply = false
readonly installedTransformers: Transformer[] = []
constructor(
private readonly token: string,
options?: ApiClientOptions,
private readonly webhookReplyEnvelope: WebhookReplyEnvelope = {}
) {
this.options = { ...DEFAULT_OPTIONS, ...options }
}
private call: ApiCallFn = async (method, payload, signal) => {
debug('Calling', method)
const url = this.options.buildUrl(
this.options.apiRoot,
this.token,
method
)
const transformed = transformPayload(method, payload ?? {})
const config = requiresFormDataUpload(transformed)
? createFormDataPayload(transformed)
: createJsonPayload(transformed)
if (
this.webhookReplyEnvelope.send !== undefined &&
!this.hasUsedWebhookReply &&
typeof config.body === 'string' &&
this.options.canUseWebhookReply(method)
) {
this.hasUsedWebhookReply = true
await this.webhookReplyEnvelope.send(config.body)
return { ok: true, result: true }
} else {
const res = await fetch(url, {
...this.options.baseFetchConfig,
signal,
...config,
})
return await res.json()
}
}
use(...transformers: Transformer[]) {
this.call = transformers.reduce(concatTransformer, this.call)
this.installedTransformers.push(...transformers)
return this
}
async callApi<M extends keyof RawApi>(
method: M,
payload: Opts<M>,
signal?: AbortSignal
) {
const data = await this.call(method, payload, signal)
if (data.ok) return data.result
else throw new GrammyError(`Call to ${method} failed!`, data, payload)
}
}
/**
* Creates a new transformable API, i.e. an object that lets you perform raw API
* calls to the Telegram Bot API server but pass the calls through a stack of
* transformers before. This will create a new API client instance under the
* hood that will be used to connect to the Telegram servers. You therefore need
* to pass the bot token. In addition, you may pass API client options as well
* as a webhook reply envelope that allows the client to perform up to one HTTP
* request in response to a webhook call if this is desired.
*
* @param token the bot's token
* @param options a number of options to pass to the created API client
* @param webhookReplyEnvelope the webhook reply envelope that will be used
*/
export function createRawApi(
token: string,
options?: ApiClientOptions,
webhookReplyEnvelope?: WebhookReplyEnvelope
): TransformableApi {
const client = new ApiClient(token, options, webhookReplyEnvelope)
const proxyHandler: ProxyHandler<RawApi> = {
get(_, m: keyof RawApi) {
return client.callApi.bind(client, m)
},
...proxyMethods,
}
const raw = new Proxy({} as RawApi, proxyHandler)
const installedTransformers = client.installedTransformers
const api: TransformableApi = {
raw,
installedTransformers,
use: (...t) => {
client.use(...t)
return api
},
}
return api
}
const proxyMethods = {
set() {
return false
},
defineProperty() {
return false
},
deleteProperty() {
return false
},
ownKeys() {
return []
},
}