Pabal API
Pabal API(MTProto)
构建像应用一样连接服务器的客户端时需要的内容:连接信息、服务器公钥、登录流程、更新和支持范围。
Pabal 应用通过 MTProto 2.0 与服务器通信。如果你要开发以同样方式连接的程序——其他应用、以真人账号运行的自动化程序、研究用客户端——请阅读本文档。如果只是开发机器人,更简单的 Bot API 就足够了。
Pabal 遵循 Telegram 的公开协议,因此协议和方法的详细定义以 MTProto 和 API 方法文档为准。本文档记录的是连接 Pabal 服务器时的不同之处以及支持范围。
连接信息
| 项目 | 值 |
|---|---|
| 地址 | 122.34.175.215 |
| 端口 | 8443(TCP) |
| DC | 1~5 都是同一个地址。连接哪个 DC 都可以,通常使用 2 |
| 协议 | MTProto 2.0,API Layer 216 |
| 传输方式 | Abridged、Intermediate、Padded Intermediate、Full——各自都包括混淆(obfuscated2)版本 |
| 服务器公钥 | server-key.pem · fingerprint 8724853375441383205 |
| api_id · api_hash | 不做校验。填任何值都可以 |
目前还没有 HTTP 传输、WebSocket 传输和 MTProxy。客户端设备的时钟必须准确——MTProto 的消息编号由时间生成,时钟偏差过大时,服务器会丢弃消息。
服务器公钥
MTProto 客户端首次连接时,会用服务器的 RSA 公钥加密授权密钥交换过程。Telegram 客户端内置的是 Telegram 的公钥,因此要连接 Pabal,就必须用这个密钥替换(或同时加入)。这个密钥可以防止其他服务器冒充 Pabal 服务器。
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA4hH74xPQsUwr/pyXPdF4tVicYr6QbfeDrKC7mUOVrPLSL4FtmgGn
w+O4u6lVvOf3Udd1KY6+OL4fUZdMBlLzwsoGLoniiVR09dnvyXHE8LhQSS+i1LmI
oJbhQwXplLnUJf272fLXkD23e7ppKLkjYk+jeYObueCy5HYMSThklVeXEzbZVGZv
47o/mjU2vyFoRpa6wCIE4rpj1UIPtOMpekMI/TocIlGVJ+ch6cAVNxIDro53a1eG
/1oZRLQH4oViEGxeMMjBMY5gk5HPkZvjbqy4h8TjXEz7O5o0BRsSqG/OPtP/dRQV
X4ewPhj2WCU4e6l5X59ZKIcv4sQLOwClqQIDAQAB
-----END RSA PUBLIC KEY-----
curl -s -o server-key.pem https://pabal.me/docs/server-key.pem
这个密钥在服务器首次启动时生成一次,之后不会改变。请确认服务器日志中 Loaded RSA key … (fingerprint …) 的值与上面的 fingerprint 一致。
用 Telethon 连接
下面是用 Python 的 Telethon 登录真人账号并收发消息的示例。请先在应用中注册账号(见下文)。
# pabal_client.py — pip install telethon==1.42.0
# 在同一文件夹中放入 server-key.pem(上面下载的服务器公钥)
import asyncio
from telethon import TelegramClient, events
from telethon.crypto import rsa
rsa.add_key(open("server-key.pem").read(), old=False) # Pabal 服务器的公钥
client = TelegramClient("pabal", api_id=1, api_hash="0" * 32) # 登录状态保存在 pabal.session 中
client.session.set_dc(2, "122.34.175.215", 8443)
@client.on(events.NewMessage(incoming=True))
async def show(event):
sender = await event.get_sender()
print(f"{sender.first_name}: {event.raw_text}")
async def main():
await client.start(phone=lambda: input("手机号码 (+86…): ")) # 第一次需要输入一次验证码
me = await client.get_me()
print(f"已登录:{me.first_name} (id {me.id})")
await client.send_message("BotFather", "/help")
await client.run_until_disconnected()
asyncio.run(main())
- 请使用 Telethon 1.42。这是使用 Layer 216 的版本。更新的版本会尝试用更高的 Layer 解析响应,结果会因
TypeNotFoundError而失败。 - Telethon 屏蔽了新账号注册(
sign_up())。请在应用中注册,或者直接调用auth.signUp。 - 登录状态保存在
pabal.session文件中,之后不会再询问验证码。这个文件就是账号的钥匙,请妥善保管。
登录流程
图示说明
- 两个阶段:上方是生成加密所用授权密钥的阶段(协议),下方是把账号绑定到该密钥上的登录阶段(API)。上方的阶段由库自动完成。
- 登录绑定在授权密钥上:用一个已经登录的密钥打开多个会话,这些会话全都处于登录状态。丢失密钥(删除会话文件)后需要重新登录。
- 验证码的发送途径由服务器运营方决定:短信或管理员转交时返回
sentCodeTypeSms;电子邮件时返回sentCodeTypeSetUpEmailRequired,客户端会询问电子邮件地址并调用account.sendVerifyEmailCode(中间一行)。 - 红色虚线是新号码的路径:验证码正确但账号不存在时,会返回
authorizationSignUpRequired,填入名字并调用auth.signUp即可完成注册。只有在验证码输入正确之后才能注册。 - 机器人不走下方的阶段,而是用一次
auth.importBotAuthorization(机器人令牌)登录。
| 错误 | 何时出现 |
|---|---|
PHONE_NUMBER_INVALID | 号码有误,或者在服务器关闭新注册的状态下使用了新号码 |
PHONE_NUMBER_BANNED | 被运营方封禁的号码,或已停用账号的号码(若在停用状态下注销,默认 1 年——运营方可以延长或设为无限期) |
FLOOD_WAIT_n (420) | 请求验证码过于频繁。请在 n 秒后重试 |
PHONE_CODE_INVALID | 验证码错误(超过规定次数后会被锁定) |
PHONE_CODE_EXPIRED | 验证码已过期或被锁定,或者 phone_code_hash 未知——请从 sendCode 重新开始 |
EMAIL_INVALID, EMAIL_NOT_ALLOWED | 电子邮件地址有误 / 已有账号,但该地址不是已登记的登录邮箱 |
AUTH_KEY_UNREGISTERED (401) | 用未登录的密钥调用了需要登录的方法 |
在运营方开启了测试号码的服务器上,+99966XYYYY 形式的号码无需实际发送验证码,用验证码 XXXXX(X 重复五次)即可登录。这是只用于开发服务器的功能,在生产服务器上是关闭的。
接收更新
- 实时:连接保持打开时,服务器会立即以
updateShortMessage、updates发送新消息、修改和删除。发出请求的会话会通过 RPC 响应收到结果,所以同样的内容不会再以推送形式重复送达。 - pts:每个用户的变化都带有序号(pts)。如果客户端收到的 pts 出现空缺,就说明错过了什么。
- 补齐:先用
updates.getState记下当前的 pts,重新连接后用updates.getDifference(pts, date, qts)获取这段时间内的新消息、删除以及相关的用户和群组。如果客户端持有的 pts 比服务器还超前(例如服务器数据被重置),会返回differenceTooLong,这时请重新加载对话列表。 - 推送通知——Android:没有连接的 Android 应用会被 Firebase 推送唤醒。应用用
account.registerDevice(token_type2 = FCM 令牌,secret= 256 字节的设备密钥)登记后,服务器会用该设备密钥加密新消息和来电并通过 FCM 发送,被唤醒的应用再用updates.getDifference补齐。推送只能送达用服务器运营方的 Firebase 项目构建的应用。 - 推送通知——iPhone:iPhone 应用用同一个请求登记两样东西——
token_type1(APNs 通知令牌)和 9(PushKit VoIP 令牌),两者的secret都是该账号的 256 字节设备密钥。新消息以 APNs 通知发送:Apple 能看到的外层文字只有应用名称和“You have a new notification”这类不含内容的话,消息本身用设备密钥加密后放在p中发送,由应用的通知扩展(Notification Service Extension)解密并显示。来电通过 VoIP 推送调出 CallKit 来电界面(从运营方启用 VoIP 的构建开始;在此之前以通知发送),在其他设备上已读或删除的内容则用后台推送告知。推送只能送达与服务器运营方的 APNs 密钥属于同一团队签名、且 Bundle ID 相同的应用。其他token_type只以boolTrue应答,并不发送。 - Telethon 之类的库会自动完成上述全部过程。
ID 与对端
| 对象 | ID | 备注 |
|---|---|---|
| 用户 | 从 100001 开始 | peerUser。@BotFather 为 100000 |
| 机器人 | 与用户使用同一套编号 | user.bot = true。令牌前面的数字就是机器人 ID |
| 普通群组 | 从 1000001 开始 | peerChat。在 Bot API 中显示为负数(-chat_id) |
| 消息 | 整个服务器共用一个序列 | 一对一对话的两份副本编号相同,因此只接收消息编号、不指定对端的请求(messages.getMessages、messages.deleteMessages)对两个人指向的也是同一条消息。只有频道在各自频道内从 1 单独计数 |
请把服务器给出的 access_hash 原样保存下来再使用(它会随用户名查询、对话列表和更新一起送达)。
文件
- 上传:用
upload.saveFilePart分块上传(每块最大 512KB,最多 4,000 块),再用inputFileUploaded… 引用。超过 10MB 的文件用upload.saveBigFilePart上传——因为会预先告知分块数量,所以如果服务器不接受该大小,会在传完之前就以FILE_TOO_BIG拒绝。分块一到达就写入磁盘,等到有消息引用该文件时再合并成一个文件。 - 发送:在
messages.sendMedia中使用inputMediaUploadedPhoto(新照片)、inputMediaPhoto(服务器上已有的照片)、inputMediaUploadedDocument(新文件)、inputMediaDocument(服务器上已有的文件)。其余媒体返回MEDIA_INVALID。也可以用messages.uploadMedia先上传而不发送(返回的 media 之后原样发出即可),messages.sendMultiMedia虽然会被接受,但其中各项是逐条分开发出的,不会打包成一组(没有 grouped_id)。 - 下载:在
upload.getFile中使用inputPhotoFileLocation(消息中的照片)、inputDocumentFileLocation(消息中的文件)、inputPeerPhotoFileLocation(头像)。每次最多 1MB。下载时也不会把整个文件读进内存,只读取请求的那一段。 - 头像:
photos.uploadProfilePhoto、photos.updateProfilePhoto、photos.getUserPhotos、photos.deletePhotos。 - 照片只以原图一种尺寸保存(不单独生成缩略图)。文件则原封不动地保留,发送方附上的
documentAttribute*会按字节原样转交给接收方——服务器无需理解视频的时长或语音的波形,只负责如实传递。
支持范围
服务器为 Layer 216 的 469 个方法都提供了处理程序,全部能够读取请求,但用真实客户端端到端验证过的是下面这些方法。其余方法的响应格式正确,但内容可能为空,或者不会被记录。
| 领域 | 已验证的方法 |
|---|---|
| 连接 | initConnection、invokeWithLayer、help.getConfig、auth.bindTempAuthKey(PFS 临时密钥)、auth.exportAuthorization/importAuthorization |
| 登录 | auth.sendCode、auth.signIn、auth.signUp、auth.logOut、auth.importBotAuthorization、account.sendVerifyEmailCode |
| 用户、联系人 | users.getUsers、users.getFullUser、contacts.resolveUsername、contacts.importContacts、contacts.search |
| 消息 | messages.sendMessage、messages.sendMedia(照片、文件)、messages.getHistory、messages.getDialogs、messages.getMessages、messages.editMessage、messages.deleteMessages |
| 群组 | messages.createChat、messages.deleteChatUser、messages.editChatTitle(messages.addChatUser 只用官方应用验证过) |
| 机器人 | messages.getBotCallbackAnswer、messages.setBotCallbackAnswer、带按钮(reply_markup)的消息 |
| 更新 | updates.getState、updates.getDifference、实时推送 |
| 文件、照片 | upload.saveFilePart、upload.saveBigFilePart(超过 10MB)、upload.getFile、photos.*(见上文) |
我们还验证了官方 Telegram Desktop 6.2.6 无需修改即可完成注册、登录、聊天、照片、群组和重新连接。应用启动时调用的约 60 个方法,其响应格式会另外进行检查。
此后新增的内容——设备列表与结束会话(account.getAuthorizations、account.resetAuthorization、auth.resetAuthorizations)、桌面应用的数据导出(account.initTakeoutSession、invokeWithTakeout、invokeWithMessagesRange、account.finishTakeoutSession——每个账号每天 3 次,超过则返回 420 TAKEOUT_INIT_DELAY_n)、更换号码(account.sendChangePhoneCode、account.changePhone——运营方开启前返回 400 PHONE_CHANGE_DISABLED,机器人返回 BOT_METHOD_INVALID)——已通过服务器测试验证,尚未用真实应用端到端验证。
错误
错误以标准的 rpc_error(error_code + error_message)返回。error_message 始终是大写字母、数字和下划线的形式(PEER_ID_INVALID),必要时会附上 : 说明。
| 代码 | 含义 |
|---|---|
400 | 请求有误——PEER_ID_INVALID、MESSAGE_ID_INVALID、MEDIA_INVALID、USERNAME_NOT_OCCUPIED … |
401 | 需要登录——AUTH_KEY_UNREGISTERED |
403 | 没有权限——例如不是成员的群组 |
406 | 暂时不允许——登录不满 24 小时的设备试图结束其他设备时 FRESH_RESET_AUTHORISATION_FORBIDDEN |
420 | FLOOD_WAIT_n——等待 n 秒 |
500 | 服务器内部错误 |
传输错误 -404 | 服务器不认识这个授权密钥——生成新密钥后重新登录(永久密钥)或重新绑定(临时密钥) |
手表 API
服务器的 /api/watch/v1/ 是 Pabal Apple Watch 应用专用的 HTTPS JSON 入口。因为手表不能使用 MTProto,所以单独设置了它;它不是公开规范,而且仍在准备中,可能会不经通知就更改。要编写连接 Pabal 的程序,请使用本页介绍的 MTProto。服务器运营方请参阅服务器安装——Apple Watch 入口。
尚不支持的功能
- 频道和超级群组(
channels.*会响应,但在应用中无法正常显示)、私密聊天、群组通话 - 能在应用内直接播放的视频和语音消息、显示为一组的相册(grouped_id)、贴纸、缩略图(文件只有原件)
- 两步验证(SRP)——无法设置,需要两步验证的操作会被拒绝
- HTTP 和 WebSocket 传输、MTProxy、发送
msgs_ack、定期更换服务器 salt(salt 错误时以bad_server_salt告知正确值) - 拆分到多台服务器——由一台服务器同时承担 DC 1~5
让官方 Telegram 应用连接 Pabal
Telegram 应用在构建时就把服务器地址和公钥内置进去。因此不能在设置界面中修改,而是需要修改源代码后重新构建。Pabal 应用(Pabal.app)就是这样做出来的。如果你是服务器运营方,请参阅服务器安装——连接应用。