Effectful library for crafting Telegram bots.
# Install the library
npm install @grom.js/effect-tg
# Install Effect dependencies
npm install effect @effect/platformBotApi service provides access to Telegram's Bot API.
Each method on BotApi corresponds to the Bot API method with typed parameters and results.
Methods return an Effect that succeeds with the method result or fails with BotApiError (see "Error handling").
Example: Calling Bot API methods using BotApi.
import { BotApi } from '@grom.js/effect-tg'
import { Effect } from 'effect'
const program = Effect.gen(function* () {
const api = yield* BotApi.BotApi
const me = yield* api.getMe()
yield* api.sendMessage({
chat_id: 123456789,
text: `Hello from ${me.username}!`,
})
})Alternatively, you can use BotApi.callMethod function to call any method by name.
Example: Calling Bot API methods using BotApi.callMethod.
import { BotApi } from '@grom.js/effect-tg'
import { Effect } from 'effect'
const program = Effect.gen(function* () {
const me = yield* BotApi.callMethod('getMe')
yield* BotApi.callMethod('sendMessage', {
chat_id: 123456789,
text: `Hello from ${me.username}!`,
})
})BotApi has a layered architecture:
ββ’ BotApi β typed interface that delegates calls to BotApiTransport.
βββ¬β’ BotApiTransport β serializes parameters, sends HTTP requests, parses responses.
ββββ’ BotApiUrl β constructs endpoint URLs to methods and files.
ββββ’ HttpClient β performs HTTP requests.
This design enables:
- Extensibility: Extend
BotApiTransportto implement logging, retrying, etc. - Testability: Mock implementation of
BotApiTransportorHttpClientto test your bot. - Portability: Provide different
BotApiUrlto run a bot on test environment or with local Bot API server.
Example: Constructing BotApi layer with method call tracing.
import { FetchHttpClient } from '@effect/platform'
import { BotApi } from '@grom.js/effect-tg'
import { Config, Effect, Layer } from 'effect'
const BotApiLive = Layer.provide(
BotApi.layerConfig({
token: Config.redacted('BOT_TOKEN'),
environment: 'prod',
transformTransport: transport => ({
sendRequest: (method, params) =>
transport.sendRequest(method, params).pipe(
Effect.withSpan(method),
),
}),
}),
FetchHttpClient.layer
)Failed BotApi method calls result in BotApiError, which is a union of tagged errors with additional information:
TransportErrorβ HTTP or network failure.causeβ original error fromHttpClient.
RateLimitedβ bot has exceeded the flood limit.retryAfterβ duration to wait before the next attempt.
GroupUpgradedβ group has been migrated to a supergroup.supergroupβ object containing the ID of the new supergroup.
MethodFailedβ response was unsuccessful, but the exact reason could not be determined.possibleReasonβ string literal representing one of the common failure reasons. It is determined by the error code and description of the Bot API response, which are subject to change.
InternalServerErrorβ Bot API server failed with a 5xx error code.
All errors except TransportError also have response property that contains the original response from Bot API.
Example: Handling Bot API failures.
import { BotApi } from '@grom.js/effect-tg'
import { Duration, Effect, Match } from 'effect'
const program = BotApi.callMethod('doSomething').pipe(
Effect.matchEffect({
onSuccess: result => Effect.logInfo('Got result:', result),
onFailure: e => Match.value(e).pipe(
Match.tagsExhaustive({
TransportError: ({ message }) =>
Effect.logError(`Probably network issue: ${message}`),
RateLimited: ({ retryAfter }) =>
Effect.logError(`Try again in ${Duration.format(retryAfter)}`),
GroupUpgraded: ({ supergroup }) =>
Effect.logError(`Group now has a new ID: ${supergroup.id}`),
MethodFailed: ({ possibleReason, response }) =>
Match.value(possibleReason).pipe(
Match.when('BotBlockedByUser', () =>
Effect.logError('I was blocked...')),
Match.orElse(() =>
Effect.logError(`Unsuccessful response: ${response.description}`)),
),
InternalServerError: () =>
Effect.logError('Not much we can do about it.'),
}),
),
}),
)BotApi module exports type definitions for all Bot API types, method parameters and results.
Example: Creating custom types from Bot API types.
import { BotApi, BotApiError } from '@grom.js/effect-tg'
import { Effect } from 'effect'
// Union of all possible updates
type UpdateType = Exclude<keyof BotApi.Types.Update, 'update_id'>
// Function to get gifts of multiple chats
type GiftsCollector = (
chatIds: Array<BotApi.MethodParams['getChatGifts']['chat_id']>,
params: Omit<BotApi.MethodParams['getChatGifts'], 'chat_id'>,
) => Effect.Effect<
Array<BotApi.MethodResults['getChatGifts']>,
BotApiError.BotApiError,
BotApi.BotApi
>