{"id":44320577,"url":"https://github.com/grom-dev/effect-tg","last_synced_at":"2026-04-05T16:01:36.742Z","repository":{"id":318224441,"uuid":"1069879888","full_name":"grom-dev/effect-tg","owner":"grom-dev","description":"🛠️ Effectful library for crafting Telegram bots.","archived":false,"fork":false,"pushed_at":"2026-03-28T13:34:53.000Z","size":657,"stargazers_count":12,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-28T14:45:33.421Z","etag":null,"topics":["effect","effect-ts","telegram"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/grom-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-10-04T20:00:42.000Z","updated_at":"2026-03-28T13:34:57.000Z","dependencies_parsed_at":"2025-10-05T23:30:13.584Z","dependency_job_id":null,"html_url":"https://github.com/grom-dev/effect-tg","commit_stats":null,"previous_names":["grom-dev/effectg","grom-dev/effect-tg"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/grom-dev/effect-tg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grom-dev%2Feffect-tg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grom-dev%2Feffect-tg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grom-dev%2Feffect-tg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grom-dev%2Feffect-tg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/grom-dev","download_url":"https://codeload.github.com/grom-dev/effect-tg/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grom-dev%2Feffect-tg/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31441057,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-05T15:22:31.103Z","status":"ssl_error","status_checked_at":"2026-04-05T15:22:00.205Z","response_time":75,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["effect","effect-ts","telegram"],"created_at":"2026-02-11T06:11:58.216Z","updated_at":"2026-04-05T16:01:36.734Z","avatar_url":"https://github.com/grom-dev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# effect-tg\n\n[![Effectful](https://img.shields.io/badge/Yes.-%23fff?style=flat\u0026logo=effect\u0026logoColor=%23000\u0026logoSize=auto\u0026label=Effect%3F\u0026labelColor=%23fff\u0026color=%23000)](https://effect.website/)\n[![Bot API](https://img.shields.io/badge/v9.6-%23fff?style=flat\u0026logo=telegram\u0026logoColor=%2325A3E1\u0026logoSize=auto\u0026label=Bot%20API\u0026labelColor=%23fff\u0026color=%2325A3E1)](https://core.telegram.org/bots/api)\n[![npm](https://img.shields.io/npm/v/%40grom.js%2Feffect-tg?style=flat\u0026logo=npm\u0026logoColor=%23BB443E\u0026logoSize=auto\u0026label=Latest\u0026labelColor=%23fff\u0026color=%23BB443E)](https://www.npmjs.com/package/@grom.js/effect-tg)\n[![codecov](https://img.shields.io/codecov/c/github/grom-dev/effect-tg?style=flat\u0026logo=codecov\u0026logoColor=%23f07\u0026label=Coverage\u0026labelColor=%23fff\u0026color=%23f07)](https://codecov.io/gh/grom-dev/effect-tg)\n\nEffectful library for crafting Telegram bots.\n\n## Features\n\n- Modular design to build with [Effect](https://effect.website).\n- Complete type definitions for [Bot API](https://core.telegram.org/bots/api) methods and types.\n- Composable API for [sending messages](#sending-messages).\n- [JSX syntax](#jsx-syntax) support for creating formatted text.\n\n## Installation\n\n```sh\n# Install the library\nnpm install @grom.js/effect-tg\n\n# Install Effect dependencies\nnpm install effect @effect/platform\n\n# Install JSX runtime for formatted text\nnpm install @grom.js/tgx\n```\n\n## Working with Bot API\n\n### Calling methods\n\n`BotApi` service provides access to Telegram's Bot API.\nEach method on `BotApi` corresponds to the Bot API method with typed parameters and results.\nMethods return an `Effect` that succeeds with the method result or fails with `BotApiError` (see [\"Error handling\"](#error-handling)).\n\n**Example:** Calling Bot API methods using `BotApi`.\n\n```ts\nimport { BotApi } from '@grom.js/effect-tg'\nimport { Effect } from 'effect'\n\nconst program = Effect.gen(function* () {\n  const api = yield* BotApi.BotApi\n  const me = yield* api.getMe()\n  yield* api.sendMessage({\n    chat_id: 123456789,\n    text: `Hello from ${me.username}!`,\n  })\n})\n```\n\nAlternatively, you can use `BotApi.callMethod` function to call any method by name.\n\n**Example:** Calling Bot API methods using `BotApi.callMethod`.\n\n```ts\nimport { BotApi } from '@grom.js/effect-tg'\nimport { Effect } from 'effect'\n\nconst program = Effect.gen(function* () {\n  const me = yield* BotApi.callMethod('getMe')\n  yield* BotApi.callMethod('sendMessage', {\n    chat_id: 123456789,\n    text: `Hello from ${me.username}!`,\n  })\n})\n```\n\n### Configuration\n\n`BotApi` has a layered architecture:\n\n```text\n┌• BotApi — typed interface that delegates calls to BotApiTransport.\n└─┬• BotApiTransport — serializes parameters, sends HTTP requests, parses responses.\n  ├──• BotApiUrl — constructs endpoint URLs to methods and files.\n  └──• HttpClient — performs HTTP requests.\n```\n\nThis design enables:\n\n- **Extensibility**: Extend `BotApiTransport` to implement logging, retrying, etc.\n- **Testability**: Mock implementation of `BotApiTransport` or `HttpClient` to test your bot.\n- **Portability:** Provide different `BotApiUrl` to run a bot on test environment or with local Bot API server.\n\n**Example:** Constructing `BotApi` layer with method call tracing.\n\n```ts\nimport { FetchHttpClient } from '@effect/platform'\nimport { BotApi } from '@grom.js/effect-tg'\nimport { Config, Effect, Layer } from 'effect'\n\nconst BotApiLive = Layer.provide(\n  BotApi.layerConfig({\n    token: Config.redacted('BOT_TOKEN'),\n    environment: 'prod',\n    transformTransport: transport =\u003e ({\n      sendRequest: (method, params) =\u003e\n        transport.sendRequest(method, params).pipe(\n          Effect.withSpan(method),\n        ),\n    }),\n  }),\n  FetchHttpClient.layer\n)\n```\n\n### Error handling\n\nFailed `BotApi` method calls result in `BotApiError`, which is a union of tagged errors with additional information:\n\n- **`TransportError`** — HTTP or network failure. The `cause` property contains the original error from `HttpClient`.\n- **`RateLimited`** — bot has exceeded the flood limit. The `retryAfter` property contains the duration to wait before the next attempt.\n- **`GroupUpgraded`** — group has been migrated to a supergroup. The `supergroup` property contains an object with the ID of the new supergroup.\n- **`MethodFailed`** — response was unsuccessful, but the exact reason could not be determined. The `possibleReason` property contains a 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.\n- **`InternalServerError`** — Bot API server failed with a 5xx error code.\n\nAll errors except `TransportError` also have `response` property that contains the original response from Bot API.\n\n**Example:** Handling Bot API failures.\n\n```ts\nimport { BotApi } from '@grom.js/effect-tg'\nimport { Duration, Effect, Match } from 'effect'\n\nconst program = BotApi.callMethod('doSomething').pipe(\n  Effect.matchEffect({\n    onSuccess: result =\u003e Effect.logInfo('Got result:', result),\n    onFailure: e =\u003e Match.value(e).pipe(\n      Match.tagsExhaustive({\n        TransportError: ({ message }) =\u003e\n          Effect.logError(`Probably network issue: ${message}`),\n        RateLimited: ({ retryAfter }) =\u003e\n          Effect.logError(`Try again in ${Duration.format(retryAfter)}`),\n        GroupUpgraded: ({ supergroup }) =\u003e\n          Effect.logError(`Group is now a supergroup with ID: ${supergroup.id}`),\n        MethodFailed: ({ possibleReason, response }) =\u003e\n          Match.value(possibleReason).pipe(\n            Match.when('BotBlockedByUser', () =\u003e\n              Effect.logError('I was blocked...')),\n            Match.orElse(() =\u003e\n              Effect.logError(`Unsuccessful response: ${response.description}`)),\n          ),\n        InternalServerError: () =\u003e\n          Effect.logError('Not much we can do about it.'),\n      }),\n    ),\n  }),\n)\n```\n\n### Types\n\n`BotApi` module exports type definitions for all Bot API types, method parameters and results.\n\n**Example:** Creating custom types from Bot API types.\n\n```ts\nimport { BotApi, BotApiError } from '@grom.js/effect-tg'\nimport { Effect } from 'effect'\n\n// Union of all possible updates\ntype UpdateType = Exclude\u003ckeyof BotApi.Types.Update, 'update_id'\u003e\n\n// Function to get gifts of multiple chats\ntype GiftsCollector = (\n  chatIds: Array\u003cBotApi.MethodParams['getChatGifts']['chat_id']\u003e,\n  params: Omit\u003cBotApi.MethodParams['getChatGifts'], 'chat_id'\u003e,\n) =\u003e Effect.Effect\u003c\n  Array\u003cBotApi.MethodResults['getChatGifts']\u003e,\n  BotApiError.BotApiError,\n  BotApi.BotApi\n\u003e\n```\n\n## Sending messages\n\nOne of the most common tasks for a messenger bot is sending messages.\n\nBot API exposes multiple methods for sending a message, each corresponding to a different content type:\n\n- `sendMessage` for text;\n- `sendPhoto` for photos;\n- `sendVideo` for videos;\n- and so on.\n\n`Send` module provides a unified, more composable way to send messages of all kinds.\n\n### Basic usage\n\nTo send a message, you need:\n\n- **Content** — content of the message to be sent.\n- **Dialog** — target chat and topic where the message will be sent.\n- **Markup** — (optional) markup for replying to the message.\n- **Reply** — (optional) information about the message being replied to.\n- **Options** — (optional) additional options for sending the message.\n\n`Send.sendMessage` function accepts mentioned parameters and returns an `Effect` that sends a message, automatically choosing the appropriate Bot API method based on the content type.\n\n**Example:** Sending messages using `Send.sendMessage`.\n\n```ts\nimport { Content, Dialog, File, Markup, Reply, Send, Text } from '@grom.js/effect-tg'\nimport { Effect } from 'effect'\n\nconst program = Effect.gen(function* () {\n  // Plain text to a user\n  const greeting = yield* Send.sendMessage({\n    content: Content.text(Text.plain('Hey! Wanna roll a dice?')),\n    dialog: Dialog.user(382713),\n  })\n\n  // Photo with formatted caption and inline keyboard\n  yield* Send.sendMessage({\n    content: Content.photo(\n      File.External(new URL('https://cataas.com/cat')),\n      { caption: Text.html('\u003cb\u003eCat of the day\u003c/b\u003e\\n\u003ci\u003eRate this cat:\u003c/i\u003e') },\n    ),\n    dialog: Dialog.user(382713),\n    markup: Markup.inlineKeyboard([\n      [Markup.InlineButton.callback('❤️', 'rate_love')],\n      [Markup.InlineButton.callback('👎', 'rate_nope')],\n    ]),\n  })\n\n  // Reply with a dice\n  const roll = yield* Send.sendMessage({\n    content: Content.dice('🎲'),\n    dialog: Dialog.user(382713),\n    reply: Reply.toMessage(greeting),\n  })\n\n  const rolled = roll.dice!.value\n  if (rolled === 6) {\n    // DM channel\n    yield* Send.sendMessage({\n      content: Content.text(Text.plain(`User 382713 rolled ${rolled}.`)),\n      dialog: Dialog.channelDm(Dialog.channel(100200), 42),\n    })\n  }\n  else {\n    // Send silently\n    yield* Send.sendMessage({\n      content: Content.text(Text.plain(`You rolled ${rolled}. Disappointing.`)),\n      dialog: Dialog.user(382713),\n      options: Send.options({ disableNotification: true })\n    })\n  }\n})\n```\n\n#### Content\n\n`Content` module provides constructors for creating objects that represent the content of a message. `Send.sendMessage` uses the content type to choose the appropriate Bot API method automatically.\n\n| Constructor            | Bot API method  | Description            |\n| ---------------------- | --------------- | ---------------------- |\n| `Content.text`         | `sendMessage`   | Text                   |\n| `Content.photo`        | `sendPhoto`     | Photo                  |\n| `Content.video`        | `sendVideo`     | Video                  |\n| `Content.animation`    | `sendAnimation` | GIF or video w/o sound |\n| `Content.audio`        | `sendAudio`     | Audio file             |\n| `Content.voice`        | `sendVoice`     | Voice note             |\n| `Content.videoNote`    | `sendVideoNote` | Round video note       |\n| `Content.document`     | `sendDocument`  | File of any type       |\n| `Content.sticker`      | `sendSticker`   | Sticker                |\n| `Content.location`     | `sendLocation`  | Static location        |\n| `Content.liveLocation` | `sendLocation`  | Live location          |\n| `Content.venue`        | `sendVenue`     | Venue with address     |\n| `Content.contact`      | `sendContact`   | Phone contact          |\n| `Content.dice`         | `sendDice`      | Random dice            |\n\n#### Dialog\n\n`Dialog` module provides utilities for creating target chats:\n\n- `Dialog.user(id)` — private chat with a user.\n- `Dialog.group(id)` — chat of a (basic) group.\n- `Dialog.supergroup(id)` — supergroup chat.\n- `Dialog.channel(id)` — channel.\n\nTargeting a specific topic:\n\n- `Dialog.privateTopic(user, topicId)` — topic in a private chat.\n- `Dialog.forumTopic(supergroup, topicId)` — topic in a forum supergroup.\n- `Dialog.channelDm(channel, topicId)` — channel direct messages.\n\n`Dialog.ofMessage` helper extracts the dialog from an incoming `Message` object.\n\n##### Dialog and peer IDs\n\nBot API uses a single integer to encode peer type with its ID — [dialog ID](https://core.telegram.org/api/bots/ids).\n\nThis may not be a problem for user IDs, since user IDs map to the same dialog IDs.\nHowever, this may cause some defects when working with other peers.\nFor example, to send a message to a channel with ID `3011378744`, you need to set `chat_id` parameter to `-1003011378744`.\n\nTo prevent this confusion, `Dialog` module defines **branded types** that distinguish peer IDs from dialog IDs at the type level:\n\n- `UserId` — number representing a user ID.\n- `GroupId` — number representing a group ID.\n- `ChannelId` — number representing a channel ID.\n- `SupergroupId` — alias to `ChannelId`, since supergroups share ID space with channels.\n- `DialogId` — number encoding peer type and peer ID.\n\nConstructors like `Dialog.user`, `Dialog.channel`, etc. validate and encode IDs internally, so you rarely need to convert manually. When you do, `Dialog` module exports conversion utilities:\n\n- `Dialog.decodeDialogId(dialogId)` — decodes a dialog ID into peer type and peer ID.\n- `Dialog.decodePeerId(peer, dialogId)` — extracts a typed peer ID from a dialog ID.\n- `Dialog.encodePeerId(peer, id)` — encodes a peer type and ID into a dialog ID.\n\n#### Markup\n\n`Markup` module provides reply markup types and constructors:\n\n- `Markup.inlineKeyboard(rows)` — inline keyboard attached to the message.\n- `Markup.replyKeyboard(rows, options?)` — custom keyboard for quick reply or other action.\n- `Markup.replyKeyboardRemove()` — hide a previously shown reply keyboard.\n- `Markup.forceReply()` — forces Telegram client to reply to the message.\n\n**Example:** Creating reply markups.\n\n```ts\nimport { Markup } from '@grom.js/effect-tg'\n\nconst inline = Markup.inlineKeyboard([\n  [Markup.InlineButton.callback('Like', 'liked')],\n  [Markup.InlineButton.url('Source code', 'https://github.com/grom-dev/effect-tg')],\n])\n\nconst reply = Markup.replyKeyboard(\n  [\n    ['Option A', 'Option B'],\n    [Markup.ReplyButton.requestContact('Share phone')],\n  ],\n  { oneTime: true, resizable: true }\n)\n```\n\n### Prepared messages\n\n`Send.message` creates a `MessageToSend` — prepared message that bundles content, markup, reply, and options.\n\n`MessageToSend` is also an `Effect`, which means:\n\n- It can be piped to chain modifiers that customize markup, reply, and options.\n- It can be executed to send the message. To be sent, `Send.TargetDialog` service should be provided.\n\n**Example:** Creating and sending prepared messages.\n\n```ts\nimport { Content, Dialog, Markup, Send, Text } from '@grom.js/effect-tg'\nimport { Effect } from 'effect'\n\n// Reusable template\nconst welcomeMessage = Send.message(\n  Content.text(Text.html('\u003cb\u003eWelcome!\u003c/b\u003e Thanks for joining.'))\n).pipe(\n  Send.withMarkup(\n    Markup.replyKeyboard([\n      [Markup.ReplyButton.text('Effect?')],\n      [Markup.ReplyButton.text('Die.')],\n    ]),\n  ),\n)\n\n// Send to different dialogs\nconst greet1 = Effect.gen(function* () {\n  yield* welcomeMessage.pipe(Send.to(Dialog.user(123)))\n  yield* welcomeMessage.pipe(Send.to(Dialog.channel(321)))\n})\n\n// Send to the same dialog with different options\nconst greet2 = Effect.gen(function* () {\n  yield* welcomeMessage.pipe(Send.withoutNotification)\n  yield* welcomeMessage.pipe(Send.withContentProtection)\n}).pipe(\n  Send.to(Dialog.forumTopic(Dialog.supergroup(4), 2)),\n)\n```\n\n### Composing options\n\nChain modifiers on a `MessageToSend` to customize its behavior:\n\n- `withMarkup`/`withoutMarkup` — set/remove reply markup.\n- `withReply`/`withoutReply` — set/remove reply options.\n- `withNotification`/`withoutNotification` — enable/disable notification sound.\n- `withContentProtection`/`withoutContentProtection` — prevent/allow forwarding and saving.\n- `withPaidBroadcast`/`withoutPaidBroadcast` — enable/disable paid broadcast.\n- `withOptions` — merge with the new send options.\n\n**Example:** Chaining modifiers on a prepared message.\n\n```ts\nimport { Content, Markup, Send, Text } from '@grom.js/effect-tg'\n\nconst secretPromo = Send.message(Content.text(Text.plain('Shh!'))).pipe(\n  Send.withMarkup(\n    Markup.inlineKeyboard([\n      [Markup.InlineButton.copyText('Copy promo', 'EFFECT_TG')],\n    ]),\n  ),\n  Send.withoutNotification,\n  Send.withContentProtection,\n)\n```\n\n### Text formatting\n\n`Text` module provides utilities for creating [formatted text](https://core.telegram.org/bots/api#formatting-options) to be used in text messages and captions.\n\n**Example:** Formatting text with `Text` module.\n\n```tsx\nimport { Text } from '@grom.js/effect-tg'\n\n// Plain text — sent as is\nText.plain('*Not bold*. _Not italic_.')\n\n// Markdown — sent with 'MarkdownV2' parse mode\nText.markdown('*Bold* and _italic_.')\n\n// HTML — sent with 'HTML' parse mode\nText.html('\u003cb\u003eBold\u003c/b\u003e and \u003ci\u003eitalic\u003c/i\u003e.')\n```\n\n#### JSX syntax\n\n`Text` module also allows to compose formatted text with JSX.\n\nBenefits of using JSX:\n\n- **Validation**: JSX is validated during compilation, so you can't specify invalid HTML or Markdown.\n- **Composability**: JSX allows composing formatted text with custom components.\n- **Auto-escaping**: JSX escapes special characters, saving you from \\\u003cs\\\u003ebAd\\\u003c/s\\\u003e \\_iNpUtS\\_.\n- **Type safety**: LSP hints and type checking for text entities and custom components.\n\n`Text.tgx` function accepts a JSX element and returns an instance of `Text.Tgx`, which can then be used as a content of a message.\n\n\u003cdetails\u003e\n\u003csummary\u003eHow to enable?\u003c/summary\u003e\n\n1. Install `@grom.js/tgx` package:\n\n   ```sh\n   npm install @grom.js/tgx\n   ```\n\n2. Update your `tsconfig.json`:\n\n   ```json\n   {\n     \"compilerOptions\": {\n       \"jsx\": \"react-jsx\",\n       \"jsxImportSource\": \"@grom.js/tgx\"\n     }\n   }\n   ```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eHow it works?\u003c/summary\u003e\n\nJSX is just syntactic sugar transformed by the compiler.\nResult of transformation depends on the JSX runtime.\n`effect-tg` relies on JSX runtime from [`@grom.js/tgx`](https://github.com/grom-dev/tgx), which transforms JSX elements to `TgxElement` instances.\nWhen `Send.sendMessage` encounters an instance of `Text.Tgx`, it converts inner `TgxElement`s to the parameters for a `send*` method.\n\n\u003c/details\u003e\n\n**Example:** Composing reusable messages with JSX.\n\n```tsx\nimport type { PropsWithChildren } from '@grom.js/tgx'\nimport { Content, Dialog, Send, Text } from '@grom.js/effect-tg'\n\n// Reusable component for a key-value field\nconst Field = (props: PropsWithChildren\u003c{ label: string }\u003e) =\u003e (\n  \u003c\u003e\u003cb\u003e{props.label}:\u003c/b\u003e {props.children}{'\\n'}\u003c/\u003e\n)\n\n// Simple component for convenience\nconst RocketEmoji = () =\u003e (\n  \u003cemoji id=\"5445284980978621387\" alt=\"🚀\" /\u003e\n)\n\n// Component that renders a deploy summary\nconst DeploySummary = (props: {\n  service: string\n  version: string\n  env: string\n  author: string\n  url: string\n}) =\u003e (\n  \u003c\u003e\n    \u003cRocketEmoji /\u003e \u003cb\u003eDeploy to \u003ci\u003e{props.env}\u003c/i\u003e\u003c/b\u003e\n    {'\\n\\n'}\n    \u003cField label=\"Service\"\u003e\u003ccode\u003e{props.service}\u003c/code\u003e\u003c/Field\u003e\n    \u003cField label=\"Version\"\u003e\u003ccode\u003e{props.version}\u003c/code\u003e\u003c/Field\u003e\n    \u003cField label=\"Author\"\u003e{props.author}\u003c/Field\u003e\n    {'\\n'}\n    \u003ca href={props.url}\u003eView in dashboard\u003c/a\u003e\n    {'\\n\\n'}\n    \u003cblockquote expandable\u003e\n      Changelog:{'\\n'}\n      - Fix rate limiting on /api/submit{'\\n'}\n      - Add retry logic for webhook delivery{'\\n'}\n      - Update dependencies\n    \u003c/blockquote\u003e\n  \u003c/\u003e\n)\n\n// Create summary text\nconst summary = Text.tgx(\n  \u003cDeploySummary\n    service=\"billing-api\"\n    version=\"2.14.0\"\n    env=\"production\"\n    author=\"Alice\"\n    url=\"https://deploy.example.com/runs/4821\"\n  /\u003e\n)\n\n// Publish a new post\nconst publish = Send.message(Content.text(summary)).pipe(\n  Send.to(Dialog.channel(3011378744)),\n)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrom-dev%2Feffect-tg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrom-dev%2Feffect-tg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrom-dev%2Feffect-tg/lists"}