Add initial Telegram relay service for Iran→foreign delivery.
Thin Hono API so the Iran-hosted backend can POST ops alerts here and this VPS calls Telegram Bot API.
This commit is contained in:
commit
54a8dcb283
17
.env.example
Normal file
17
.env.example
Normal file
@ -0,0 +1,17 @@
|
||||
# HTTP
|
||||
PORT=3100
|
||||
HOST=0.0.0.0
|
||||
|
||||
# Shared secret between Iran backend and this relay (≥32 chars).
|
||||
# Backend must send header: X-Ghabilee-Telegram-Relay-Secret
|
||||
RELAY_SECRET=change-me-to-a-long-random-secret-at-least-32
|
||||
|
||||
# Telegram Bot API (same bot used by Ghabilee ops alerts)
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
|
||||
# Default destination: prefer group; private chat is fallback
|
||||
TELEGRAM_GROUP_CHAT_ID=
|
||||
TELEGRAM_CHAT_ID=
|
||||
|
||||
# Optional default forum topic (ops). Callers may override per request.
|
||||
TELEGRAM_GROUP_THREAD_ID=
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
coverage/
|
||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN addgroup -S relay && adduser -S relay -G relay
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY package.json ./
|
||||
USER relay
|
||||
EXPOSE 3100
|
||||
CMD ["node", "dist/index.js"]
|
||||
99
README.md
Normal file
99
README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Ghabilee Telegram Relay
|
||||
|
||||
Thin HTTP service that lives on a **foreign** VPS and forwards ops alerts to
|
||||
Telegram Bot API. The Iran-hosted Nest backend calls this relay instead of
|
||||
`api.telegram.org` directly (which is often unreachable from inside Iran).
|
||||
|
||||
Repo: <https://git.ghabilee.ir/AliSaZa/telegrambot.git>
|
||||
|
||||
## Why
|
||||
|
||||
- Core product (Jibit, Kavenegar, Postgres) stays in Iran.
|
||||
- Only Telegram delivery needs outbound access to `api.telegram.org`.
|
||||
- Failures here must not break user flows (backend already treats most alerts as best-effort).
|
||||
|
||||
## API
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Liveness for Docker/load balancers. No auth.
|
||||
|
||||
### `POST /v1/send`
|
||||
|
||||
Auth header (required):
|
||||
|
||||
```http
|
||||
X-Ghabilee-Telegram-Relay-Secret: <RELAY_SECRET>
|
||||
```
|
||||
|
||||
Body (JSON):
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
| ----- | ---- | -------- | ----- |
|
||||
| `text` | string | yes | 1–4096 chars (Telegram limit) |
|
||||
| `chatId` | string | no | Override default chat from env |
|
||||
| `messageThreadId` | number \| null | no | Forum topic; `null` = no topic; omit = env default |
|
||||
| `disableWebPagePreview` | boolean | no | Default `true` |
|
||||
|
||||
Success:
|
||||
|
||||
```json
|
||||
{ "ok": true, "messageId": 123 }
|
||||
```
|
||||
|
||||
Errors: `401` unauthorized, `400` validation, `502` Telegram API failure.
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://relay.example.com/v1/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Ghabilee-Telegram-Relay-Secret: $RELAY_SECRET" \
|
||||
-d '{"text":"hello from relay","messageThreadId":8}'
|
||||
```
|
||||
|
||||
## Local run
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# fill TELEGRAM_BOT_TOKEN, chat ids, RELAY_SECRET (≥32 chars)
|
||||
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Tests / typecheck:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## Env
|
||||
|
||||
See [`.env.example`](.env.example). Production needs:
|
||||
|
||||
- `RELAY_SECRET` (≥32)
|
||||
- `TELEGRAM_BOT_TOKEN`
|
||||
- `TELEGRAM_GROUP_CHAT_ID` (preferred) or `TELEGRAM_CHAT_ID`
|
||||
- optional `TELEGRAM_GROUP_THREAD_ID` (ops forum topic)
|
||||
|
||||
## Backend integration (next step)
|
||||
|
||||
In `ghabilee-backend` `OpsAlertsService.sendTelegram`, when
|
||||
`TELEGRAM_RELAY_URL` is set, POST to `{TELEGRAM_RELAY_URL}/v1/send` with the
|
||||
shared secret instead of calling Telegram directly. Keep a direct-Telegram
|
||||
fallback for local/dev if useful.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Do **not** put the secret in query strings (access logs).
|
||||
- Expose only HTTPS on the foreign VPS (Caddy/Nginx + Let’s Encrypt).
|
||||
- Firewall: ideally allow only the Iran VPS egress IP to hit `/v1/send`.
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
services:
|
||||
telegram-relay:
|
||||
build: .
|
||||
ports:
|
||||
- '${PORT:-3100}:3100'
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD',
|
||||
'wget',
|
||||
'-qO-',
|
||||
'http://127.0.0.1:3100/health',
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
604
package-lock.json
generated
Normal file
604
package-lock.json
generated
Normal file
@ -0,0 +1,604 @@
|
||||
{
|
||||
"name": "ghabilee-telegram-relay",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ghabilee-telegram-relay",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.14.4",
|
||||
"hono": "^4.7.11",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.32",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.17",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
|
||||
"integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz",
|
||||
"integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
|
||||
"integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.23.13",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
|
||||
"integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "ghabilee-telegram-relay",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "External Telegram sendMessage relay for Ghabilee ops alerts (Iran backend → foreign VPS).",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "tsx --test src/**/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.14.4",
|
||||
"hono": "^4.7.11",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.32",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
46
src/app.test.ts
Normal file
46
src/app.test.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import { createApp, RELAY_SECRET_HEADER } from './app.js'
|
||||
import type { RelayConfig } from './config.js'
|
||||
|
||||
const baseConfig: RelayConfig = {
|
||||
port: 3100,
|
||||
host: '127.0.0.1',
|
||||
relaySecret: 'a'.repeat(32),
|
||||
telegramBotToken: 'test-token',
|
||||
defaultChatId: '-100123',
|
||||
defaultThreadId: 8,
|
||||
}
|
||||
|
||||
describe('POST /v1/send', () => {
|
||||
it('rejects missing secret', async () => {
|
||||
const app = createApp(baseConfig)
|
||||
const res = await app.request('/v1/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: 'hello' }),
|
||||
})
|
||||
assert.equal(res.status, 401)
|
||||
})
|
||||
|
||||
it('rejects short/invalid body', async () => {
|
||||
const app = createApp(baseConfig)
|
||||
const res = await app.request('/v1/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
[RELAY_SECRET_HEADER]: baseConfig.relaySecret,
|
||||
},
|
||||
body: JSON.stringify({ text: '' }),
|
||||
})
|
||||
assert.equal(res.status, 400)
|
||||
})
|
||||
|
||||
it('returns health', async () => {
|
||||
const app = createApp(baseConfig)
|
||||
const res = await app.request('/health')
|
||||
assert.equal(res.status, 200)
|
||||
const body = (await res.json()) as { ok: boolean }
|
||||
assert.equal(body.ok, true)
|
||||
})
|
||||
})
|
||||
93
src/app.ts
Normal file
93
src/app.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Hono } from 'hono'
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
import type { RelayConfig } from './config.js'
|
||||
import {
|
||||
SendMessageBodySchema,
|
||||
TelegramApiError,
|
||||
sendTelegramMessage,
|
||||
} from './telegram.js'
|
||||
|
||||
export const RELAY_SECRET_HEADER = 'x-ghabilee-telegram-relay-secret'
|
||||
|
||||
function secretsEqual(expected: string, provided: string): boolean {
|
||||
const a = Buffer.from(expected)
|
||||
const b = Buffer.from(provided)
|
||||
if (a.length !== b.length) return false
|
||||
return timingSafeEqual(a, b)
|
||||
}
|
||||
|
||||
export function createApp(config: RelayConfig) {
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/health', (c) =>
|
||||
c.json({
|
||||
ok: true,
|
||||
service: 'ghabilee-telegram-relay',
|
||||
}),
|
||||
)
|
||||
|
||||
app.post('/v1/send', async (c) => {
|
||||
const provided = c.req.header(RELAY_SECRET_HEADER)?.trim() ?? ''
|
||||
if (!provided || !secretsEqual(config.relaySecret, provided)) {
|
||||
return c.json({ ok: false, error: 'unauthorized' }, 401)
|
||||
}
|
||||
|
||||
let json: unknown
|
||||
try {
|
||||
json = await c.req.json()
|
||||
} catch {
|
||||
return c.json({ ok: false, error: 'invalid_json' }, 400)
|
||||
}
|
||||
|
||||
const parsed = SendMessageBodySchema.safeParse(json)
|
||||
if (!parsed.success) {
|
||||
return c.json(
|
||||
{
|
||||
ok: false,
|
||||
error: 'validation_failed',
|
||||
details: parsed.error.flatten(),
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
const body = parsed.data
|
||||
const chatId = body.chatId ?? config.defaultChatId
|
||||
|
||||
let messageThreadId: number | undefined
|
||||
if (body.messageThreadId === null) {
|
||||
messageThreadId = undefined
|
||||
} else if (body.messageThreadId !== undefined) {
|
||||
messageThreadId = body.messageThreadId
|
||||
} else {
|
||||
messageThreadId = config.defaultThreadId
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await sendTelegramMessage({
|
||||
botToken: config.telegramBotToken,
|
||||
chatId,
|
||||
text: body.text,
|
||||
...(messageThreadId !== undefined ? { messageThreadId } : {}),
|
||||
disableWebPagePreview: body.disableWebPagePreview,
|
||||
})
|
||||
return c.json({ ok: true, messageId: result.messageId })
|
||||
} catch (error) {
|
||||
if (error instanceof TelegramApiError) {
|
||||
console.error(
|
||||
`Telegram sendMessage failed: status=${error.status} ${error.detail}`,
|
||||
)
|
||||
return c.json(
|
||||
{ ok: false, error: 'telegram_delivery_failed' },
|
||||
502,
|
||||
)
|
||||
}
|
||||
console.error('Unexpected send failure', error)
|
||||
return c.json({ ok: false, error: 'internal_error' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.notFound((c) => c.json({ ok: false, error: 'not_found' }, 404))
|
||||
|
||||
return app
|
||||
}
|
||||
68
src/config.ts
Normal file
68
src/config.ts
Normal file
@ -0,0 +1,68 @@
|
||||
export interface RelayConfig {
|
||||
port: number
|
||||
host: string
|
||||
relaySecret: string
|
||||
telegramBotToken: string
|
||||
defaultChatId: string
|
||||
defaultThreadId: number | undefined
|
||||
}
|
||||
|
||||
function required(name: string, value: string | undefined): string {
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) {
|
||||
throw new Error(`Missing required env: ${name}`)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function optional(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim()
|
||||
return trimmed && trimmed.length > 0 ? trimmed : undefined
|
||||
}
|
||||
|
||||
function parseThreadId(raw: string | undefined): number | undefined {
|
||||
const value = optional(raw)
|
||||
if (!value) return undefined
|
||||
// Tolerate "8 # اوتومیشن" if someone pastes an inline comment.
|
||||
const withoutComment = value.split('#')[0]?.trim() ?? ''
|
||||
const threadId = Number(withoutComment)
|
||||
if (!Number.isInteger(threadId) || threadId < 1) {
|
||||
throw new Error('TELEGRAM_GROUP_THREAD_ID must be a positive integer')
|
||||
}
|
||||
return threadId
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and validate process env. Call once at boot.
|
||||
* Telegram token/chat may be empty only in health-only smoke; production requires them.
|
||||
*/
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): RelayConfig {
|
||||
const relaySecret = required('RELAY_SECRET', env.RELAY_SECRET)
|
||||
if (relaySecret.length < 32) {
|
||||
throw new Error('RELAY_SECRET must be at least 32 characters')
|
||||
}
|
||||
|
||||
const groupChatId = optional(env.TELEGRAM_GROUP_CHAT_ID)
|
||||
const privateChatId = optional(env.TELEGRAM_CHAT_ID)
|
||||
const defaultChatId = groupChatId ?? privateChatId
|
||||
if (!defaultChatId) {
|
||||
throw new Error(
|
||||
'Set TELEGRAM_GROUP_CHAT_ID or TELEGRAM_CHAT_ID as the default destination',
|
||||
)
|
||||
}
|
||||
|
||||
const portRaw = optional(env.PORT) ?? '3100'
|
||||
const port = Number(portRaw)
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new Error('PORT must be an integer between 1 and 65535')
|
||||
}
|
||||
|
||||
return {
|
||||
port,
|
||||
host: optional(env.HOST) ?? '0.0.0.0',
|
||||
relaySecret,
|
||||
telegramBotToken: required('TELEGRAM_BOT_TOKEN', env.TELEGRAM_BOT_TOKEN),
|
||||
defaultChatId,
|
||||
defaultThreadId: parseThreadId(env.TELEGRAM_GROUP_THREAD_ID),
|
||||
}
|
||||
}
|
||||
19
src/index.ts
Normal file
19
src/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { serve } from '@hono/node-server'
|
||||
import { createApp } from './app.js'
|
||||
import { loadConfig } from './config.js'
|
||||
|
||||
const config = loadConfig()
|
||||
const app = createApp(config)
|
||||
|
||||
serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port: config.port,
|
||||
hostname: config.host,
|
||||
},
|
||||
(info) => {
|
||||
console.log(
|
||||
`ghabilee-telegram-relay listening on http://${info.address}:${info.port}`,
|
||||
)
|
||||
},
|
||||
)
|
||||
74
src/telegram.ts
Normal file
74
src/telegram.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/** Telegram Bot API sendMessage text limit. */
|
||||
export const TELEGRAM_TEXT_MAX = 4096
|
||||
|
||||
export const SendMessageBodySchema = z.object({
|
||||
text: z.string().trim().min(1).max(TELEGRAM_TEXT_MAX),
|
||||
/** Override default chat from env (group or private). */
|
||||
chatId: z.string().trim().min(1).optional(),
|
||||
/** Forum topic id; omit to use env default (if any). Pass null to force no topic. */
|
||||
messageThreadId: z.number().int().positive().nullable().optional(),
|
||||
disableWebPagePreview: z.boolean().optional().default(true),
|
||||
})
|
||||
|
||||
export type SendMessageBody = z.infer<typeof SendMessageBodySchema>
|
||||
|
||||
export interface TelegramSendResult {
|
||||
ok: true
|
||||
messageId: number
|
||||
}
|
||||
|
||||
export class TelegramApiError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly detail: string,
|
||||
) {
|
||||
super(`Telegram sendMessage failed: status=${status}`)
|
||||
this.name = 'TelegramApiError'
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendTelegramMessage(input: {
|
||||
botToken: string
|
||||
chatId: string
|
||||
text: string
|
||||
messageThreadId?: number
|
||||
disableWebPagePreview: boolean
|
||||
fetchImpl?: typeof fetch
|
||||
}): Promise<TelegramSendResult> {
|
||||
const fetchImpl = input.fetchImpl ?? fetch
|
||||
const payload: Record<string, unknown> = {
|
||||
chat_id: input.chatId,
|
||||
text: input.text,
|
||||
disable_web_page_preview: input.disableWebPagePreview,
|
||||
}
|
||||
if (input.messageThreadId !== undefined) {
|
||||
payload.message_thread_id = input.messageThreadId
|
||||
}
|
||||
|
||||
const response = await fetchImpl(
|
||||
`https://api.telegram.org/bot${input.botToken}/sendMessage`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => '')
|
||||
throw new TelegramApiError(response.status, detail.slice(0, 300))
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
ok?: boolean
|
||||
result?: { message_id?: number }
|
||||
}
|
||||
const messageId = json.result?.message_id
|
||||
if (!json.ok || typeof messageId !== 'number') {
|
||||
throw new TelegramApiError(502, 'Unexpected Telegram API response')
|
||||
}
|
||||
|
||||
return { ok: true, messageId }
|
||||
}
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user