goletter/hyperf-telegram-bot

Telegram Bot API SDK for Hyperf

Maintainers

Package info

github.com/goletter/hyperf-telegram-bot

pkg:composer/goletter/hyperf-telegram-bot

Transparency log

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-08-14 15:59 UTC

This package is auto-updated.

Last update: 2026-08-14 15:59:50 UTC


README

Hyperf 协程友好的 Telegram Bot API 客户端,支持多 Bot、动态 Token、Webhook 校验与常用消息能力。

心智模型

角色 作用
工厂 Goletter\Telegram\Factory\BotFactory 唯一推荐入口:按 Token / 名称拿到 Bot
客户端 Goletter\Telegram\Bot 调用 Telegram API(发消息、设 Webhook 等)
解析 Helper\Webhook + Update\Update 接收推送、校验 secret、读取命令 / chat_id
中间件 Middleware\VerifyTelegramWebhookMiddleware 只做 secret 校验;动态多 Bot 需先 attach

请注入 BotFactory。包默认不绑定 BotInterface(未配置静态 Token 时注入会失败)。

安装

本仓库(已通过 PSR-4 引入)

composer.json

"Goletter\\Telegram\\": "packages/goletter/hyperf-telegram-bot/src/"

config/autoload/annotations.php 扫描路径需包含:

BASE_PATH . '/packages/goletter/hyperf-telegram-bot/src',

发布配置(若尚未有 config/autoload/telegram.php):

php bin/hyperf.php vendor:publish goletter/hyperf-telegram-bot

独立项目

composer require goletter/hyperf-telegram-bot
php bin/hyperf.php vendor:publish goletter/hyperf-telegram-bot

配置

config/autoload/telegram.php 主要只配 HTTP。多机器人、Token 存在数据库时,不必写死 bots

return [
    'default' => env('TELEGRAM_BOT', 'default'),
    'bots' => [
        // 可选静态 Bot;动态场景可留空
    ],
    'http' => [
        'base_uri' => env('TELEGRAM_API_BASE_URI', 'https://api.telegram.org'),
        'timeout' => (float) env('TELEGRAM_HTTP_TIMEOUT', 30),
        'proxy' => env('TELEGRAM_HTTP_PROXY'), // 例如 http://127.0.0.1:7890
    ],
];

静态单 Bot 时可这样写:

'bots' => [
    'default' => [
        'token' => env('TELEGRAM_BOT_TOKEN', ''),
        'webhook_secret' => env('TELEGRAM_WEBHOOK_SECRET', ''),
    ],
],

Token 格式必须是 {bot_id}:{secret},例如 7123456789:AAHxxxx。不要只填 secret 段,也不要把整段 API URL 塞进来。

场景 A:动态多 Bot(推荐)

Token 来自数据库 / 业务配置,可随时变更。同名 Bot 在 Token 变化时会自动重建缓存。

use Goletter\Telegram\Factory\BotFactory;
use Hyperf\Di\Annotation\Inject;

#[Inject]
protected BotFactory $bots;

// 按 Token 创建(默认缓存名:bot:{bot_id},如 123456:ABC -> bot:123456)
$bot = $this->bots->token($row->token);

// 推荐:用业务 ID 作为名称,便于 Token 轮换后命中同一缓存槽
$bot = $this->bots->token($row->token, (string) $row->id, [
    'webhook_secret' => (string) $row->webhook_secret,
]);

$bot->sendMessage([
    'chat_id' => 123456,
    'text' => 'Hello',
]);

// Token 更新后再 resolve,旧实例自动失效
$bot = $this->bots->resolve((string) $row->id, $newToken, [
    'webhook_secret' => $newSecret,
]);

// 主动丢弃缓存
$this->bots->forget((string) $row->id);

BotFactory 方法怎么选

方法 何时用
token($token) 临时用,缓存名默认 bot:{bot_id}
token($token, $bizId, $options) 业务侧推荐,名称稳定、便于轮换
resolve($name, $token, $options) 显式按名称绑定;Token 变了会重建
get('default') 仅当配置文件里有静态 bots.xxx.token
make(...) 只创建、不进缓存(一次性调用)
forget($name) 主动清缓存;forget() 清空全部

$options 目前支持:webhook_secret(设置 Webhook / 校验推送时用)。

场景 B:静态单 Bot

telegram.bots.default.token 配好后:

$bot = $this->bots->get('default');
// 或省略名称,取 telegram.default
$bot = $this->bots->get();

如仍希望注入 BotInterface,在项目 config/autoload/dependencies.php 自行绑定:

use Goletter\Telegram\Contract\BotInterface;
use Goletter\Telegram\Factory\BotFactory;

return [
    BotInterface::class => static fn ($c) => $c->get(BotFactory::class)->get(),
];

场景 C:Webhook(完整可跑)

1. 路由建议

POST /telegram/webhook/{id},用业务 ID 区分机器人。

2. 控制器(推荐写法)

在控制器里用 BotFactory 拿到 Bot,再交给 Webhook::parseRequest(会校验 X-Telegram-Bot-Api-Secret-Token)。

use Goletter\Telegram\Factory\BotFactory;
use Goletter\Telegram\Helper\Webhook;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Contract\RequestInterface;

#[Controller]
class TelegramWebhookController
{
    public function __construct(
        protected BotFactory $bots,
        protected Webhook $webhook,
        protected BotRepository $repo, // 你们自己的仓储
    ) {
    }

    #[PostMapping(path: '/telegram/webhook/{id}')]
    public function handle(int $id, RequestInterface $request)
    {
        $entity = $this->repo->find($id);

        $bot = $this->bots->token($entity->token, (string) $entity->id, [
            'webhook_secret' => $entity->webhook_secret,
        ]);

        $update = $this->webhook->parseRequest($request, $bot);

        if ($update->isCommand('start')) {
            $bot->sendMessage([
                'chat_id' => $update->getChatId(),
                'text' => 'Welcome!',
            ]);
        }

        return ['ok' => true];
    }
}

3. 设置 Webhook

每个机器人各自 URL。若创建 Bot 时带了 webhook_secretsetWebhook 会自动附带 secret_token

$bot->setWebhook([
    'url' => 'https://example.com/telegram/webhook/' . $entity->id,
    'allowed_updates' => ['message', 'callback_query', 'my_chat_member', 'chat_member', 'chat_join_request'],
]);

4. 可选:中间件校验

VerifyTelegramWebhookMiddleware 不会自己从数据库查 Token。动态多 Bot 时,必须先在业务中间件里 attach

// 业务中间件(在 VerifyTelegramWebhookMiddleware 之前)
$entity = $repo->find($request->getAttribute('id'));
$request = $webhook->attach($request, $entity->token, (string) $entity->id, [
    'webhook_secret' => $entity->webhook_secret,
]);

控制器里再解析:

$update = $this->webhook->parseRequest($request); // 从 request attribute 取 Bot
$bot = $this->webhook->bot(null, $request);

静态配置且路由带 {bot} 参数时,中间件可直接按配置名 get($bot),无需 attach

场景 D:Long Polling

$updates = $bot->getUpdates([
    'offset' => $offset,
    'timeout' => 30,
]);

foreach ($updates as $item) {
    $update = $bot->parseUpdate($item);
    // 使用 $update->getChatId() / isCommand() 等
}

Webhook 与 Long Polling 不要同时用于同一 Bot。

常用能力

发送消息 / 文件

$bot->sendMessage([
    'chat_id' => 123456,
    'text' => 'Hello',
]);

$bot->sendDocument([
    'chat_id' => 123456,
    'document' => fopen('/path/to/file.pdf', 'r'),
    'caption' => '报表',
]);
// document 也支持 SplFileInfo,或 ['contents' => ..., 'filename' => 'a.pdf']

任意未封装的 API 可用:

$bot->call('sendVenue', [/* ... */]);

获取群用户(重要限制)

Telegram Bot API 不能一次拉全群成员,只能:

  1. 查人数 / 管理员
  2. 按已知 user_id 查单个或批量
  3. 监听 chat_member 进退群事件自行落库后再查
$chatId = -1001234567890;

$users = $bot->getGroupUsers($chatId);
// ['chat_id' => ..., 'count' => 128, 'administrators' => [...]]

$count = $bot->getGroupMemberCount($chatId);
$admins = $bot->getGroupAdmins($chatId);
$member = $bot->getGroupMember($chatId, 123456789);
// $member['status'] => creator|administrator|member|restricted|left|kicked

$map = $bot->getGroupMembers($chatId, [111, 222, 333]);
// 不在群内 / 查询失败的为 null

$chat = $bot->getChat(['chat_id' => $chatId]);

$info = $bot->getGroupInfo($chatId);
// ['chat_id' => ..., 'chat' => [...], 'count' => 128, 'administrators' => [...]]

$map = $bot->getGroupsInfo([-100111, -100222]);
// chat_id 字符串键 => 资料|null

监听成员变动并落库(Webhook 需允许 chat_member):

if ($changed = $update->getChatMemberUpdate()) {
    $user = $changed['new_chat_member']['user'];
    $status = $changed['new_chat_member']['status']; // member / left / ...
    // 写入群成员表,之后用 getGroupMembers 回查
}

按 Bot 获取已加入的群(需自行落库)

Telegram 没有「列出该 Bot 所在全部群」的 API。本包提供:

组件 作用
BotChatTracker 从 Update 同步,再按 Bot 查询群列表
BotChatRepositoryInterface 仓储契约;默认内存实现,生产请换数据库
getMyChatMember() / isMyChatMemberUpdate() 识别 Bot 进退群事件
getGroupInfo / getGroupsInfo 已知 chat_id 时拉实时资料

Webhook 控制器里同步:

use Goletter\Telegram\Service\BotChatTracker;

public function __construct(
    protected BotFactory $bots,
    protected Webhook $webhook,
    protected BotChatTracker $chats,
    protected BotRepository $repo,
) {
}

public function handle(int $id, RequestInterface $request)
{
    $entity = $this->repo->find($id);
    $bot = $this->bots->token($entity->token, (string) $entity->id, [
        'webhook_secret' => $entity->webhook_secret,
    ]);

    $update = $this->webhook->parseRequest($request, $bot);
    $this->chats->sync($bot, $update); // 写入 my_chat_member / 群消息中的 chat

    return ['ok' => true];
}

按 Bot 取群:

// 本地已同步记录
$groups = $this->chats->getGroups($bot);

// 并对每条调用 getChat + 人数等刷新(失败会标记 inactive)
$groups = $this->chats->getGroups($bot, refresh: true);

// 单群
$group = $this->chats->getGroup($bot, -1001234567890, refresh: true);

生产环境请绑定自己的仓储(内存实现重启/多 Worker 会丢):

// config/autoload/dependencies.php
use Goletter\Telegram\Contract\BotChatRepositoryInterface;
use App\Telegram\DbBotChatRepository;

return [
    BotChatRepositoryInterface::class => DbBotChatRepository::class,
];

设置 Webhook 时务必包含 my_chat_member,否则 Bot 被拉进/踢出群时收不到事件。

拉群 / 邀请链接

Bot 不能直接把用户拉进群,需具备管理员的 can_invite_users,通过邀请链接或审批加群申请完成。

$link = $bot->inviteToChat(-1001234567890, [
    'name' => '活动拉群',
    'member_limit' => 100,
    'expire_date' => time() + 86400,
    // 'creates_join_request' => true,
]);
// $link['invite_link'] => https://t.me/+xxxx

$url = $bot->exportChatInviteLink(['chat_id' => -1001234567890]);

if ($join = $update->getChatJoinRequest()) {
    $bot->approveChatJoinRequest([
        'chat_id' => $update->getChatId(),
        'user_id' => $update->getUserId(),
    ]);
    // 或:$bot->declineChatJoinRequest([...]);
}

$bot->revokeChatInviteLink([
    'chat_id' => -1001234567890,
    'invite_link' => $link['invite_link'],
]);

Update 常用方法

方法 说明
getUpdateId() update_id
getMessage() message / edited_message / channel_post 等
getCallbackQuery() 回调查询
getChatJoinRequest() 加群申请
getChatMemberUpdate() chat_member / my_chat_member
getMyChatMember() 仅 my_chat_member(Bot 进退群)
isMyChatMemberUpdate() 是否为 Bot 自身成员变更
getChatId() / getUserId() 会话与用户 ID
getText() 消息文本或 callback data
isCommand('start') 是否为 /start(兼容 /start@BotName
toArray() 原始数组

异常

API 失败抛出 Goletter\Telegram\Exceptions\TelegramApiException

use Goletter\Telegram\Exceptions\TelegramApiException;

try {
    $bot->sendMessage(['chat_id' => 1, 'text' => 'hi']);
} catch (TelegramApiException $e) {
    $e->getDescription();
    $e->getErrorCode();
    $e->getParameters(); // 如 retry_after
    $e->getResponse();
}

Webhook secret 校验失败同样抛该异常(HTTP 语义上对应 403)。