From b0049f2cbd106f8509d902b70a58b0805a6b5eab Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sat, 12 Sep 2026 16:23:50 +0800 Subject: [PATCH 01/10] Include request details in static.yml for deployment Added request details for GitHub Pages deployment. --- .github/workflows/static.yml | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..1dc7ed4 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,56 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: '.' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + + Request URL: https://oauth.telegram.org/.well-known/openid-configuration +Request method: POST +Accept: */* +Content-Type: application/x-www-form-urlencoded +User-Agent: GitHub-Hookshot/8a6c86d +X-Github-Delivery: c5e7ad62-ae82-11f1-997e-7dd9f6a6c463 +X-Github-Event: ping +X-Github-Hook-Id: 678080962 +X-Github-Hook-Installation-Target-Id: 1367094186 +X-Github-Hook-Installation-Target-Type: repository +X-Hub-Signature: sha1=9bbc4a8c63131d059bb4f16899a444b8fe197da4 +X-Hub-Signature-256: sha256=c6cdfe2cbd0c167cafa5d42279f2a2cee9fdee3b363d6002aead25b1f559cb53 From a8332512c02749ff629b5f3dff78e05a86582221 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sat, 12 Sep 2026 16:57:58 +0800 Subject: [PATCH 02/10] Add Docker Image CI workflow --- .github/workflows/docker-image.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..3f53646 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) From 81205ad8e6da76652ef01a28bde6e0e8f08c85a2 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sun, 13 Sep 2026 06:08:25 +0800 Subject: [PATCH 03/10] Delete .github/workflows/static.yml Docker image of the Telegram Bot API server, by GramIO. Multi-arch, non-root, signed, auto-updating. --- .github/workflows/static.yml | 56 ------------------------------------ 1 file changed, 56 deletions(-) delete mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml deleted file mode 100644 index 1dc7ed4..0000000 --- a/.github/workflows/static.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - # Upload entire repository - path: '.' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 - - Request URL: https://oauth.telegram.org/.well-known/openid-configuration -Request method: POST -Accept: */* -Content-Type: application/x-www-form-urlencoded -User-Agent: GitHub-Hookshot/8a6c86d -X-Github-Delivery: c5e7ad62-ae82-11f1-997e-7dd9f6a6c463 -X-Github-Event: ping -X-Github-Hook-Id: 678080962 -X-Github-Hook-Installation-Target-Id: 1367094186 -X-Github-Hook-Installation-Target-Type: repository -X-Hub-Signature: sha1=9bbc4a8c63131d059bb4f16899a444b8fe197da4 -X-Hub-Signature-256: sha256=c6cdfe2cbd0c167cafa5d42279f2a2cee9fdee3b363d6002aead25b1f559cb53 From ecc12cad3656f624bd0c4f7bc50f6b41105706fb Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Mon, 14 Sep 2026 23:32:28 +0800 Subject: [PATCH 04/10] Add files via upload --- local.md | 280 ++++++++++++++++++++++++++++++++++ tdlight (1).md | 397 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 677 insertions(+) create mode 100644 local.md create mode 100644 tdlight (1).md diff --git a/local.md b/local.md new file mode 100644 index 0000000..4671621 --- /dev/null +++ b/local.md @@ -0,0 +1,280 @@ +--- +url: 'https://gramio.dev/bot-api/local.md' +--- + +# Local Bot API Server + +By default GramIO talks to Telegram's cloud Bot API at `https://api.telegram.org`. You can instead run **your own** [Telegram Bot API server](https://github.com/tdlib/telegram-bot-api) and point GramIO at it. + +GramIO publishes a ready-to-use image: **`ghcr.io/gramiojs/telegram-bot-api`** (also on Docker Hub as `gramiojs/telegram-bot-api`) — multi-arch, non-root, healthchecked, signed, and rebuilt automatically when upstream updates. + +> \[!TIP] +> Need **user mode** (drive a real account as a userbot), message search, `getChats`, or message scheduling? Those live in the [tdlight fork](/bot-api/tdlight) — use the typed [`@gramio/tdlight`](/bot-api/tdlight) layer. For a normal bot with bigger file limits, the official image on this page is the simpler, battle-tested choice. + +## Why self-host + +| | Cloud API | Local server (`--local`) | +|---|---|---| +| Upload | 50 MB | **2 GB** | +| Download | 20 MB | **unlimited** | +| Upload from disk | — | **`file://` local path** | +| `getFile` result | download URL | **absolute path on disk** | +| Webhooks | HTTPS, fixed ports | **HTTP, any IP/port** | + +If you only need small files and standard limits, the cloud API is simpler — stick with it. + +## Prerequisites + +You need **two different credentials** — don't mix them up: + +| Credential | Identifies | Where to get it | +|---|---|---| +| `BOT_TOKEN` | your **bot** | [@BotFather](https://t.me/BotFather) → `/newbot` (you already have this) | +| `api_id` + `api_hash` | your **application** (required to run a local server) | [my.telegram.org](https://my.telegram.org) — steps below | + +**Getting `api_id` / `api_hash` (one-time, ~1 min):** + +1. Open [my.telegram.org](https://my.telegram.org) and log in with **your Telegram account's phone number** (your own account, *not* the bot — a login code arrives in your Telegram). +2. Click **API development tools**. +3. Fill in any **App title** and **Short name** (platform: *Other*) → **Create application**. +4. Copy **`api_id`** (a number) and **`api_hash`** (a long string). Keep the hash secret. + +Then **log out of the cloud API first** — a bot token can't be used on the cloud and a local server at the same time. Call [`logOut`](/telegram/methods/logOut) once, then start your local server. Cloud login stays unavailable for ~10 minutes afterwards. + +```ts twoslash +import { Bot } from "gramio"; + +const cloudBot = new Bot(process.env.BOT_TOKEN as string); +// ---cut--- +// Run once on the cloud API, before switching to your local server +await cloudBot.api.logOut(); +``` + +## Run the server + +::: code-group + +```sh [docker run] +docker run -d --name telegram-bot-api \ + -e TELEGRAM_API_ID=123456 \ + -e TELEGRAM_API_HASH=your_api_hash \ + -p 8081:8081 \ + -v telegram-bot-api-data:/var/lib/telegram-bot-api \ + ghcr.io/gramiojs/telegram-bot-api:latest +``` + +```yaml [docker-compose.yml] +services: + telegram-bot-api: + image: ghcr.io/gramiojs/telegram-bot-api:latest + restart: unless-stopped + environment: + TELEGRAM_API_ID: ${TELEGRAM_API_ID} + TELEGRAM_API_HASH: ${TELEGRAM_API_HASH} + volumes: + - telegram-bot-api-data:/var/lib/telegram-bot-api + ports: + - "8081:8081" + +volumes: + telegram-bot-api-data: +``` + +::: + +The image runs with `--local` enabled by default. Set `TELEGRAM_LOCAL=0` to keep the cloud-style URL download flow (see [Downloading files](#downloading-files)). + +The image is a small (~50 MB) Alpine build, multi-arch (`amd64` + `arm64`), non-root, and signed. + +## Connect GramIO + +Point `api.baseURL` at your server. **Keep the `/bot` suffix** — GramIO appends the token to it. + +```ts twoslash +import { Bot } from "gramio"; +// ---cut--- +const bot = new Bot(process.env.BOT_TOKEN as string, { + api: { + baseURL: "http://localhost:8081/bot", + }, +}); +``` + +In Docker, use the service name instead of `localhost`, e.g. `http://telegram-bot-api:8081/bot`. + +## Downloading files + +This is the part that trips people up. In `--local` mode, [`getFile`](/telegram/methods/getFile) returns an **absolute path on the server's filesystem** (e.g. `/var/lib/telegram-bot-api//documents/file_5.jpg`) — **not** a download URL. + +If your bot runs in a **separate container/host from the server** (the common case), it can't read that path off disk, and `ctx.download()` / `bot.downloadFile()` — which build a `…/file/bot/` URL — won't work against `--local`. + +### Easiest: the bundled file server (`FILE_SERVER=1`) + +The image can serve files itself — no sidecar, no extra service. Set `FILE_SERVER=1` and the container also runs an nginx that serves the working dir over HTTP at **path-based, token-less** URLs (`http://host:8080//documents/file.jpg`). It's **off by default**; one env var turns it on. This is the simplest option on single-image platforms (Coolify, Dokploy, Railway, …): + +```sh +docker run -d --name telegram-bot-api \ + -e TELEGRAM_API_ID=123456 \ + -e TELEGRAM_API_HASH=your_api_hash \ + -e FILE_SERVER=1 \ + -p 8081:8081 -p 8080:8080 \ + -v telegram-bot-api-data:/var/lib/telegram-bot-api \ + ghcr.io/gramiojs/telegram-bot-api:latest +``` + +Then turn the absolute `file_path` into a download URL (prefix swap), pointing at the file-server port: + +```ts +const file = await bot.api.getFile({ file_id: ctx.document!.fileId }); +const rel = file.file_path!.replace("/var/lib/telegram-bot-api/", ""); +const url = `http://telegram-bot-api:8080/${rel}`; // token-less +``` + +`FILE_SERVER_PORT` (default `8080`) changes the port. Want one process per container instead? Use the nginx **sidecar** below. + +### Alternative: a separate nginx sidecar + +Run an `nginx` sidecar that shares the server's working-dir volume **read-only** and serves it over HTTP. The URLs are path-based, so the **bot token never appears in them**. + +`nginx/telegram-files.conf`: + +```nginx +server { + listen 80; + location / { + root /var/lib/telegram-bot-api; # mounted read-only + autoindex off; + } +} +``` + +Compose overlay: + +```yaml +services: + nginx: + image: nginx:alpine + depends_on: + telegram-bot-api: + condition: service_healthy + volumes: + - telegram-bot-api-data:/var/lib/telegram-bot-api:ro + - ./nginx/telegram-files.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "8080:80" +``` + +Then turn the absolute `file_path` into a download URL by swapping the working-dir prefix for the nginx base URL: + +```ts +import { Bot } from "gramio"; + +const FILES_BASE_URL = "http://localhost:8080"; +const WORK_DIR = "/var/lib/telegram-bot-api"; + +const bot = new Bot(process.env.BOT_TOKEN as string, { + api: { baseURL: "http://telegram-bot-api:8081/bot" }, +}); + +bot.on("message", async (ctx) => { + const fileId = ctx.document?.fileId; + if (!fileId) return; + + const file = await bot.api.getFile({ file_id: fileId }); + if (!file.file_path) return; + + // /var/lib/telegram-bot-api/123/documents/x.pdf -> http://localhost:8080/123/documents/x.pdf + const rel = file.file_path.replace(`${WORK_DIR}/`, ""); + const url = `${FILES_BASE_URL}/${rel}`; + + await ctx.reply(`Download: ${url}`); // no token in this link +}); +``` + +nginx serves [range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests) natively, so multi-GB downloads resume correctly. Want access control? Add HTTP basic auth, an IP allow-list, or nginx `secure_link` — all optional and off by default. + +### Alternatives + +* **Bot shares the volume.** If the bot container mounts the same volume, just read `file.file_path` off disk with `fs`/`Bun.file` — no nginx needed. +* **Disable `--local`.** With `TELEGRAM_LOCAL=0` the server downloads files itself and serves them at the familiar `…/file/bot/` URL, so `ctx.download()` keeps working — but you lose 2 GB uploads and unlimited downloads. + +## Uploading large files + +A local server raises the upload limit to **2 GB** (the cloud API caps documents at 50 MB — no way around that without a local server). Against a local server a **normal upload works up to 2 GB** — nothing special required: + +```ts +import { Bot, MediaUpload } from "gramio"; + +const bot = new Bot(process.env.BOT_TOKEN as string, { + api: { baseURL: "http://localhost:8081/bot" }, +}); + +bot.on("message", (ctx) => + // streamed over HTTP to your local server — up to 2 GB + ctx.sendDocument(MediaUpload.path("./big-archive.zip")), +); +``` + +If the file already lives **on the server's own disk** (bot co-located, or a shared volume), `MediaUpload.localPath()` is an **optimization** — the server reads it directly via the `file://` scheme, so the bytes never travel over HTTP: + +```ts +ctx.sendDocument(MediaUpload.localPath("/var/data/big-archive.zip")); +``` + +See the [media upload guide](/files/media-upload) and [`sendDocument`](/telegram/methods/sendDocument). + +> \[!TIP] +> For very large files prefer `MediaUpload.stream(...)` over `MediaUpload.path(...)` — the latter currently reads the whole file into memory. + +## Webhooks + +A local server accepts **HTTP** webhooks on any IP and port (the cloud API requires HTTPS on a fixed set of ports). See [Webhook](/updates/webhook). + +## Environment variables + +Every `telegram-bot-api` option is exposed as an environment variable — the rule is `--some-option` → `TELEGRAM_SOME_OPTION`. + +| Variable | Flag | Default | Description | +|---|---|---|---| +| `TELEGRAM_API_ID` *(required)* | `--api-id` | — | application identifier from [my.telegram.org](https://my.telegram.org) | +| `TELEGRAM_API_HASH` *(required)* | `--api-hash` | — | application hash from [my.telegram.org](https://my.telegram.org) | +| `TELEGRAM_LOCAL` | `--local` | `1` | enable local mode (set `0` to disable) | +| `FILE_SERVER` | *(bundled nginx)* | `0` | serve the working dir over HTTP (`1` to enable) | +| `FILE_SERVER_PORT` | *(bundled nginx)* | `8080` | port for the bundled file server | +| `TELEGRAM_WORK_DIR` | `--dir` | `/var/lib/telegram-bot-api` | server working directory | +| `TELEGRAM_TEMP_DIR` | `--temp-dir` | `/tmp/telegram-bot-api` | directory for temporary files | +| `TELEGRAM_HTTP_PORT` | `--http-port` | `8081` | HTTP listening port | +| `TELEGRAM_STAT_PORT` | `--http-stat-port` | `8082` | HTTP statistics port (the healthcheck uses it) | +| `TELEGRAM_HTTP_IP_ADDRESS` | `--http-ip-address` | — | local IP to accept HTTP connections on | +| `TELEGRAM_HTTP_STAT_IP_ADDRESS` | `--http-stat-ip-address` | — | local IP to accept statistics connections on | +| `TELEGRAM_FILTER` | `--filter` | — | `/` — shard bots across servers | +| `TELEGRAM_MAX_WEBHOOK_CONNECTIONS` | `--max-webhook-connections` | — | default max webhook connections per bot | +| `TELEGRAM_MAX_CONNECTIONS` | `--max-connections` | — | maximum number of open file descriptors | +| `TELEGRAM_PROXY` | `--proxy` | — | HTTP proxy for outgoing webhook requests (`http://host:port`) | +| `TELEGRAM_LOG_FILE` | `--log` | — | path to the log file | +| `TELEGRAM_LOG_MAX_FILE_SIZE` | `--log-max-file-size` | `2000000000` | max log file size in bytes before rotation | +| `TELEGRAM_VERBOSITY` | `--verbosity` | — | log verbosity level | +| `TELEGRAM_MEMORY_VERBOSITY` | `--memory-verbosity` | `3` | in-memory log verbosity level | +| `TELEGRAM_USERNAME` | `--username` | — | effective user name to switch to | +| `TELEGRAM_GROUPNAME` | `--groupname` | — | effective group name to switch to | +| `TELEGRAM_CPU_AFFINITY` | `--cpu-affinity` | — | CPU affinity as a 64-bit mask | +| `TELEGRAM_MAIN_THREAD_AFFINITY` | `--main-thread-affinity` | — | CPU affinity of the main thread | + +`TELEGRAM_API_ID` / `TELEGRAM_API_HASH` also accept a `_FILE` suffix to read the value from a file (Docker/Kubernetes secrets). Any extra arguments passed to the container are appended to `telegram-bot-api` verbatim. + +## Verify it works + +```sh +# stats endpoint responds +curl http://localhost:8082/ + +# container is healthy +docker inspect --format '{{.State.Health.Status}}' telegram-bot-api +``` + +## See also + +* [Download files](/files/download) — GramIO download helpers +* [`logOut`](/telegram/methods/logOut) · [`close`](/telegram/methods/close) — migrate off the cloud API +* [`getFile`](/telegram/methods/getFile) · [`sendDocument`](/telegram/methods/sendDocument) +* [tdlight Bot API Server](/bot-api/tdlight) — fork with user mode, extra methods, and unlimited file size diff --git a/tdlight (1).md b/tdlight (1).md new file mode 100644 index 0000000..dbaedd3 --- /dev/null +++ b/tdlight (1).md @@ -0,0 +1,397 @@ +--- +url: 'https://gramio.dev/bot-api/tdlight.md' +--- + +# tdlight Bot API Server + +[tdlight-telegram-bot-api](https://github.com/tdlight-team/tdlight-telegram-bot-api) is a community fork of Telegram's open-source [Bot API server](https://github.com/tdlib/telegram-bot-api), built on the lightweight **TDLight** library. It is a drop-in replacement: it speaks the standard Bot API **plus** a set of extra methods, extra fields on existing objects, and — its headline feature — an experimental **user mode** that lets you drive a real user account (a *userbot*), not just a bot. + +GramIO talks to it the same way it talks to any self-hosted server: through [`api.baseURL`](/bot-class). What was missing was **types** — calling `searchMessages` or reading `message.views` was untyped. That's what [`@gramio/tdlight`](https://github.com/gramiojs/tdlight) adds. + +> \[!IMPORTANT] +> `@gramio/tdlight` is a **types-only** package. It augments [`@gramio/types`](/types) via TypeScript declaration merging so `bot.api.*` and the response objects gain tdlight's surface. There is no runtime — you still point GramIO at a tdlight server with `api.baseURL`. New to self-hosting? Read the [Local Bot API Server](/bot-api/local) guide first; everything there (the `/bot` suffix, file-download gotcha, 2 GB uploads) applies to tdlight too. + +## Why tdlight + +The standard Bot API is deliberately narrow: a bot can only see chats it was added to, can't search, can't list a group's full membership, can't schedule messages, and can never act as a user. tdlight lifts those limits. + +| Capability | Cloud API | Official local (`--local`) | **tdlight** | +|---|---|---|---| +| 2 GB uploads, unlimited downloads, `file://` paths | — | ✅ | ✅ | +| Unlimited file **size** (`--no-file-limit`) | — | — | ✅ | +| **User mode** — act as a real account (userbot) | — | — | ✅ | +| Global message search ([`searchMessages`](#messages-scheduling)) | — | — | ✅ | +| List all your chats ([`getChats`](#chats)) | — | — | ✅ | +| Full member list ([`getChatMembers`](#members-info)) | partial | partial | ✅ (up to 200/page) | +| Message scheduling ([`send_at`](#scheduling-messages)) | — | — | ✅ | +| MTProto [proxies](#proxies) | — | — | ✅ | +| `views` / `forwards` on messages | — | — | ✅ | +| `is_scam` / `is_fake` / `is_verified` flags | — | — | ✅ | + +If you only need a normal bot with bigger file limits, the [official local server](/bot-api/local) is simpler and battle-tested — use that. Reach for tdlight when you specifically need user-mode or the extra read methods. + +> \[!WARNING] +> **User mode is experimental and risky.** Logging a real account into a third-party server and automating it can get that account **limited or banned** by Telegram, especially for bulk actions (mass joining, adding members, scraping). Use a throwaway/secondary account, keep request rates low, and never run user mode against an account you can't afford to lose. tdlight itself labels user support as experimental. + +## Install + +::: pm-add @gramio/tdlight +::: + +`@gramio/tdlight` declares [`@gramio/types`](/types) as a peer dependency — you already have it transitively through `gramio`. + +## Entry points — pick the smallest surface + +Declaration merging is **global per import**, so the package is split by mode. Import only what you need, and your `bot.api` won't be polluted with methods that can't run on your token. + +| Import | Adds to `bot.api` / objects | Use when | +|---|---|---| +| `@gramio/tdlight` | object fields + **bot-capable** methods (`ping`, `getChatMembers`, `getMessageInfo`, proxies…), scheduling params, `deleteMessages` range | A normal **bot token** against a tdlight server — the safe default | +| `@gramio/tdlight/all` | everything above **plus** user-only methods (`searchMessages`, `votePoll`, `getChats`, the auth flow…) | A **userbot** (`/user` token), which uses both surfaces | +| `@gramio/tdlight/user` | object fields + **only** the user-only methods | You want just the user-only delta | + +The import is a one-time side-effect — do it once (e.g. in your entry file) and the types apply everywhere. + +```ts +// types apply globally for the rest of your app +import "@gramio/tdlight"; +``` + +## Quick start (bot mode) + +A tdlight server in bot mode behaves like the official local server, with extra methods bolted on. + +```ts +import { Bot } from "gramio"; +import "@gramio/tdlight"; // augments bot.api with tdlight's bot-mode methods + +const bot = new Bot(process.env.BOT_TOKEN as string, { + api: { + baseURL: "http://localhost:8081/bot", // your tdlight server — keep the /bot suffix + }, +}); + +bot.command("ping", async (ctx) => { + const seconds = await bot.api.ping(); // number — MTProto round-trip + await ctx.reply(`pong in ${seconds.toFixed(3)}s`); +}); + +bot.command("admins", async (ctx) => { + const admins = await bot.api.getChatMembers({ + chat_id: ctx.chatId, + filter: "admins", + }); + await ctx.reply(`${admins.length} admins`); +}); + +bot.start(); +``` + +> The `/bot` suffix matters: GramIO builds the request URL as `${baseURL}${token}/${method}`. `"http://localhost:8081/bot"` ✅ · `"http://localhost:8081"` ❌ (the token glues to the host). See [Local Bot API Server → Connect GramIO](/bot-api/local#connect-gramio). + +## Run the tdlight server (Docker) + +tdlight publishes a multi-arch image to Docker Hub (`tdlight/tdlightbotapi`) and GHCR (`ghcr.io/tdlight-team/tdlightbotapi`). Like the official image, an entrypoint script maps `TELEGRAM_*` environment variables to CLI flags. + +You need an `api_id` / `api_hash` from [my.telegram.org](https://my.telegram.org) (identifies your *application*, not the bot) — exactly as for the [official local server](/bot-api/local#prerequisites). And as there, a bot token can't be used on the cloud and a local server at once: call [`logOut`](/telegram/methods/logOut) on the cloud API once before switching a bot over. + +::: code-group + +```sh [docker run] +docker run -d --name tdlight \ + -e TELEGRAM_API_ID=123456 \ + -e TELEGRAM_API_HASH=your_api_hash \ + -e TELEGRAM_LOCAL=1 \ + -p 8081:8081 \ + -v tdlight-data:/var/lib/telegram-bot-api \ + tdlight/tdlightbotapi:latest +``` + +```yaml [docker-compose.yml] +services: + tdlight: + image: tdlight/tdlightbotapi:latest + restart: unless-stopped + environment: + TELEGRAM_API_ID: ${TELEGRAM_API_ID} + TELEGRAM_API_HASH: ${TELEGRAM_API_HASH} + TELEGRAM_LOCAL: 1 # local mode: big files, on-disk file_path + TELEGRAM_ALLOW_USERS: 1 # enable user mode (omit for bot-only) + # TELEGRAM_ALLOW_USERS_REGISTRATION: 1 # allow registerUser (new accounts) + volumes: + - tdlight-data:/var/lib/telegram-bot-api + ports: + - "8081:8081" + +volumes: + tdlight-data: +``` + +::: + +> \[!WARNING] +> **tdlight's env-var names are its own — don't copy them from the official image.** They overlap but differ: tdlight uses `TELEGRAM_STAT` (presence-only, enables the 8082 stats port), `TELEGRAM_MAX_BATCH` (→ `--max-batch-operations`), `TELEGRAM_STAT_HIDE_SENSIBLE_DATA`, `TELEGRAM_NO_FILE_LIMIT`, etc. The HTTP port is **hardcoded to 8081** in the entrypoint — there is no `TELEGRAM_HTTP_PORT`. To change it you must pass a full command (any positional arg makes the entrypoint exec it verbatim, skipping env processing). + +Build it yourself from the repo's `Dockerfile` (Alpine, non-root) if you prefer pinning to a commit. The data dir is `/var/lib/telegram-bot-api` (same as the official image), so the [file-download nginx pattern](/bot-api/local#downloading-files) carries over unchanged. + +## User mode (userbots) + +User mode lets `@gramio/tdlight` drive a **real account**. Requests go to `/user{token}/…` instead of `/bot{token}/…`, and many extra methods (`searchMessages`, `getChats`, `votePoll`, …) only work here. Enable it on the server with `TELEGRAM_ALLOW_USERS=1` (and `TELEGRAM_ALLOW_USERS_REGISTRATION=1` only if you need to register brand-new accounts). + +### The login flow + +A user token doesn't exist up front — you obtain it by logging in. This is **not** a normal method call: login starts with an empty-method `POST` to `/user{token}/` carrying the phone number, then proceeds through code → 2FA password → (optional) registration. Each step returns an [`AuthorizationState`](#new-objects). + +Because the first step has no method name, it can't go through `bot.api.*` — you make it with a plain `fetch` (a runtime login helper is planned for a future release). Once you hold a user token, the follow-up steps **are** typed methods: + +```ts +import { Bot } from "gramio"; +import "@gramio/tdlight/all"; + +const SERVER = "http://localhost:8081"; + +// Step 1 — start login (empty method) and get a user token + state +const res = await fetch(`${SERVER}/userlogin`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ phone_number: "+1555..." }), +}); +const { result } = (await res.json()) as { + result: { token: string; authorization_state: string }; +}; +const USER_TOKEN = result.token; + +// Step 2+ — now use a typed Bot pointed at /user{token} +const userbot = new Bot(USER_TOKEN, { + api: { baseURL: `${SERVER}/user` }, // note: /user, not /bot +}); + +// submit the login code the account just received (returns the next AuthorizationState) +const afterCode = await userbot.api.authCode({ code: 12345 }); + +// if the account has 2FA enabled: +if (afterCode.authorization_state === "wait_password") { + await userbot.api.authPassword({ password: process.env.TWO_FA as string }); +} + +// the account is now logged in — drive it as a userbot +const hits = await userbot.api.searchMessages({ query: "invoice", limit: 50 }); +``` + +> \[!NOTE] +> Some standard methods are **unavailable in user mode** (a user has no `answerCallbackQuery`, `setMyCommands`, sticker-set or payments methods, and can't attach `reply_markup`). And because command messages aren't created in chats without bots, your `bot.command(...)` handlers may never fire for a userbot — drive it imperatively or with `bot.on("message", …)` instead. + +## Extra methods + +All method names below are the ones the tdlight server **actually routes** (it lowercases the incoming name). Several names in tdlight's OpenAPI spec are wrong and would `404` — `@gramio/tdlight` types the real routes. See [Method-name traps](#method-name-traps). + +### Members & info + +```ts +import "@gramio/tdlight"; + +// full member list with filter + paging (bot-capable) +const banned = await bot.api.getChatMembers({ + chat_id: -1001234567890, + filter: "banned", + offset: 0, + limit: 200, +}); + +// `getParticipants` is an alias of `getChatMembers` (same handler) +const all = await bot.api.getParticipants({ chat_id: -1001234567890 }); + +// full info for a single message +const msg = await bot.api.getMessageInfo({ chat_id: -1001234567890, message_id: 42 }); +``` + +### Chats + +User-mode (`@gramio/tdlight/all` or `/user`): + +```ts +import "@gramio/tdlight/all"; + +const chats = await userbot.api.getChats(); // all your chats +const common = await userbot.api.getCommonChats({ user_id: 777 }); // chats in common with a user +const found = await userbot.api.searchPublicChats({ query: "gramio" }); +await userbot.api.joinChat({ invite_link: "https://t.me/+abc123" }); +const created = await userbot.api.createChat({ + title: "Project X", + type: "supergroup", + description: "secret plans", +}); +``` + +### Messages & scheduling + +```ts +import "@gramio/tdlight/all"; + +const results = await userbot.api.searchMessages({ query: "deadline", limit: 100 }); +const inChat = await userbot.api.searchChatMessages({ + chat_id: -1001234567890, + query: "release", + from_user_id: 777, +}); +const scheduled = await userbot.api.getScheduledMessages({ chat_id: 777 }); +``` + +### Interaction + +```ts +import "@gramio/tdlight/all"; + +// vote in a poll ⚠ the route is `votePoll`, NOT `setPollAnswer` +await userbot.api.votePoll({ chat_id: -100..., message_id: 42, option_ids: [0, 2] }); + +// trigger a callback button and read what the originating bot would have answered +const answer = await userbot.api.getCallbackQueryAnswer({ + chat_id: -100..., + message_id: 42, + callback_data: "buy:item-7", +}); +``` + +### Proxies + +Manage the MTProto proxies the server connects through (bot-capable): + +```ts +import "@gramio/tdlight"; + +const proxy = await bot.api.addProxy({ + server: "1.2.3.4", + port: 443, + type: "mtproto", + secret: "ee...", +}); +await bot.api.enableProxy({ proxy_id: proxy.id }); +const proxies = await bot.api.getProxies(); // TdlightProxy[] +``` + +## Scheduling messages + +tdlight adds `send_at` (and `repeat_period`) to the whole send/copy/forward family. Pass a Unix timestamp (≤ 365 days out) or the string `"online"` to send when the recipient is next online. Scheduled messages get a **negative** `message_id`. + +```ts +import "@gramio/tdlight"; + +// send when the recipient comes online +await bot.api.sendMessage({ chat_id: 777, text: "ping", send_at: "online" }); + +// send at a specific time +await bot.api.sendDocument({ + chat_id: 777, + document: "BQACAgI...", + send_at: Math.floor(Date.now() / 1000) + 3600, // in 1 hour +}); + +// reschedule (or send-now) an existing scheduled message +await bot.api.editMessageScheduling({ chat_id: 777, message_id: -5, send_at: "online" }); +``` + +## Deleting message ranges + +tdlight extends [`deleteMessages`](/telegram/methods/deleteMessages) with a `start`/`end` **range** form (supergroups only, `start < end`, bounded by `--max-batch-operations`, default 10000). `@gramio/tdlight` types `start?` and `end?` on the params. + +```ts +import "@gramio/tdlight"; + +// standard form (unchanged) +await bot.api.deleteMessages({ chat_id: -100..., message_ids: [10, 11, 12] }); + +// tdlight range form — delete everything from id 100 to 500 +await bot.api.deleteMessages({ chat_id: -100..., message_ids: [], start: 100, end: 500 }); +``` + +> \[!NOTE] +> **Known limitation:** `@gramio/types` types `message_ids` as **required**, and TypeScript declaration merging can't relax a required field. So the range form still type-requires `message_ids` — pass `[]`. The clean fix belongs upstream (making `message_ids` optional in `@gramio/types`). + +## Extra object fields + +The augmentation adds tdlight's extra fields (all optional) to the objects you already read off the context, so they're typed wherever a `User` / `Chat` / `Message` / chat member appears: + +```ts +import "@gramio/tdlight"; + +bot.on("message", (ctx) => { + // User extras + if (ctx.from?.is_scam) return; // is_scam / is_fake / is_verified / is_deleted + const seen = ctx.from?.user_status; // "online" | "offline" | "recently" | ... + + // Message extras (channels) + const views = ctx.views; // number | undefined + const forwards = ctx.forwards; // number | undefined +}); +``` + +| Object | tdlight fields | +|---|---| +| `User` | `is_verified` · `is_scam` · `is_fake` · `is_deleted` · `user_status` · `last_seen` | +| `Chat` | `is_verified` · `is_scam` · `is_fake` · `distance` | +| `Message` | `views` · `forwards` · `is_scheduled` · `scheduled_at` | +| chat members | `joined_date` · `inviter` | + +### New objects + +`@gramio/tdlight` also exports three new object types (from any entry point): + +* **`AuthorizationState`** — returned by the [login flow](#the-login-flow). +* **`CallbackQueryAnswer`** — returned by `getCallbackQueryAnswer`. +* **`TdlightProxy`** — returned by `getProxies` / `addProxy`. + +```ts +import type { AuthorizationState, TdlightProxy } from "@gramio/tdlight"; +``` + +## Method-name traps + +tdlight's published OpenAPI spec documents several method names the server doesn't actually route. `@gramio/tdlight` types the **real** routes (verified against the server's C++ source), so you don't hit silent `404`s: + +| Use this ✅ | Not this ❌ (404s) | +|---|---| +| `votePoll` | `setPollAnswer` | +| `getMemoryStats` | `optimizeMemory` | +| `addChatMembers` (plural) | `addChatMember` | +| `getParticipants` (alias of `getChatMembers`) | — | +| login via empty-method `POST` | `userLogin` (no such method) | + +> \[!NOTE] +> `getMemoryStats`, `toggleGroupInvites`, and `reportChat` are **accepted but no-ops / unimplemented** on current tdlight builds — they're typed (with a JSDoc note) for completeness, but don't rely on their effects. + +## Downloading files + +File handling is identical to the [official local server](/bot-api/local#downloading-files): in `--local` mode, [`getFile`](/telegram/methods/getFile) returns an **absolute path on disk**, not a URL, so `ctx.download()` won't work against a split deployment. Serve files with the [nginx sidecar pattern](/bot-api/local#recommended-serve-files-with-nginx-no-bot-token-in-the-url) — it works unchanged because tdlight uses the same `/var/lib/telegram-bot-api` work dir. + +## Environment variables (tdlight-specific) + +The `--some-option` → `TELEGRAM_SOME_OPTION` rule mostly holds, but **tdlight has its own names** for several — don't copy them from the official image. + +| Variable | Flag | Notes | +|---|---|---| +| `TELEGRAM_API_ID` *(required)* | `--api-id` | from [my.telegram.org](https://my.telegram.org) | +| `TELEGRAM_API_HASH` *(required)* | `--api-hash` | from [my.telegram.org](https://my.telegram.org) | +| `TELEGRAM_LOCAL` | `--local` | presence-only — big files, on-disk `file_path` | +| `TELEGRAM_ALLOW_USERS` | `--allow-users` | set `1` to enable user mode | +| `TELEGRAM_ALLOW_USERS_REGISTRATION` | `--allow-users-registration` | set `1` to allow `registerUser` | +| `TELEGRAM_NO_FILE_LIMIT` | `--no-file-limit` | presence-only — remove file-size cap | +| `TELEGRAM_MAX_BATCH` | `--max-batch-operations` | range-delete cap (default 10000) | +| `TELEGRAM_HTTP_IDLE_TIMEOUT` | `--http-idle-timeout` | seconds; default 500 | +| `TELEGRAM_STAT` | `--http-stat-port=8082` | presence-only — enables the stats port | +| `TELEGRAM_STAT_HIDE_SENSIBLE_DATA` | `--stats-hide-sensible-data` | hide token/webhook on the stats page | +| `TELEGRAM_INSECURE` | `--insecure` | allow HTTP in non-local mode | +| `TELEGRAM_RELATIVE` | `--relative` | allow only relative file paths in local mode | +| `TELEGRAM_VERBOSITY` | `--verbosity` | log level (0–4, 1024) | +| `TELEGRAM_PROXY` | `--proxy` | outgoing webhook proxy | +| `TELEGRAM_WORK_DIR` | `--dir` | default `/var/lib/telegram-bot-api` | +| `TELEGRAM_TEMP_DIR` | `--temp-dir` | default `/tmp/telegram-bot-api` | + +The HTTP API port is **hardcoded to 8081** in the entrypoint (no env var). Pass `TELEGRAM_API_ID`/`TELEGRAM_API_HASH` directly — the binary reads them from the environment. + +## See also + +* [Local Bot API Server](/bot-api/local) — the official server; prerequisites, file downloads, and 2 GB uploads all apply to tdlight too +* [@gramio/types](/types) — the package `@gramio/tdlight` augments +* [Bot configuration](/bot-class) — `api.baseURL` and other options +* [`logOut`](/telegram/methods/logOut) · [`getFile`](/telegram/methods/getFile) · [`deleteMessages`](/telegram/methods/deleteMessages) From 183227d3d30178773371af8343f88445c01ee6f4 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Tue, 15 Sep 2026 00:09:54 +0800 Subject: [PATCH 05/10] Add CodeQL analysis workflow configuration --- .github/workflows/codeql.yml | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..239f5ba --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,101 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '39 13 * * 3' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From 7c9148a6e900c52205eadb2ed76fc102c3fa8ac0 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Tue, 15 Sep 2026 00:10:37 +0800 Subject: [PATCH 06/10] Add files via upload --- example-bot/telegram.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 example-bot/telegram.md diff --git a/example-bot/telegram.md b/example-bot/telegram.md new file mode 100644 index 0000000..3485430 --- /dev/null +++ b/example-bot/telegram.md @@ -0,0 +1,33 @@ + + + + + + 404 | GramIO + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file From 431b32fffaaaf87f08a6fecea97877c5271cc72e Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Tue, 15 Sep 2026 01:56:33 +0800 Subject: [PATCH 07/10] Add Jekyll GitHub Pages deployment workflow This workflow builds and deploys a Jekyll site to GitHub Pages, with necessary dependencies preinstalled. --- .github/workflows/jekyll-gh-pages.yml | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/jekyll-gh-pages.yml diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 0000000..67be9b0 --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,51 @@ +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy Jekyll with GitHub Pages dependencies preinstalled + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./ + destination: ./_site + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 From d5029e8e87a63c3459c9b49778724a562e358f57 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Tue, 15 Sep 2026 02:03:54 +0800 Subject: [PATCH 08/10] Potential fix for code scanning alert no. 2: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34d311f..51d4251 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,9 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + jobs: entrypoint: runs-on: ubuntu-latest From 72df5f52a2d5e490fd6fcaf2e106247dc6cb2fb1 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sat, 19 Sep 2026 21:10:03 +0800 Subject: [PATCH 09/10] Add Hadolint GitHub Actions workflow This workflow runs Hadolint to scan Dockerfiles for best practices and uploads the results to GitHub. --- .github/workflows/hadolint.yml | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/hadolint.yml diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml new file mode 100644 index 0000000..e0c212f --- /dev/null +++ b/.github/workflows/hadolint.yml @@ -0,0 +1,47 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# hadoint is a Dockerfile linter written in Haskell +# that helps you build best practice Docker images. +# More details at https://github.com/hadolint/hadolint + +name: Hadolint + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '35 17 * * 1' + +permissions: + contents: read + +jobs: + hadolint: + name: Run hadolint scanning + runs-on: ubuntu-latest + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run hadolint + uses: hadolint/hadolint-action@f988afea3da57ee48710a9795b6bb677cc901183 + with: + dockerfile: ./Dockerfile + format: sarif + output-file: hadolint-results.sarif + no-fail: true + + - name: Upload analysis results to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: hadolint-results.sarif + wait-for-processing: true From 9f51329fa8c1b0bb5cad371f99878ee552c34737 Mon Sep 17 00:00:00 2001 From: Marc Deniel Date: Sat, 19 Sep 2026 21:56:58 +0800 Subject: [PATCH 10/10] Potential fix for code scanning alert no. 1: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/docker-image.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 3f53646..9256486 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ "main" ] +permissions: + contents: read + jobs: build: