v0.0.31: per-bot unique user IDs, remove raw fingerprint from bot API
Privacy: from.id is now Hash(bot_token + user_fp) → different bots see different numeric IDs for the same user. Prevents cross-bot user correlation. Removed id_str (raw hex fingerprint) from all bot API responses. Updated LLM_BOT_DEV.md and LLM_HELP.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2956,7 +2956,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-client"
|
name = "warzone-client"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2989,7 +2989,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-mule"
|
name = "warzone-mule"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2998,7 +2998,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
@@ -3023,7 +3023,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-server"
|
name = "warzone-server"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3053,7 +3053,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-wasm"
|
name = "warzone-wasm"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.30"
|
version = "0.0.31"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"
|
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"
|
||||||
|
|||||||
@@ -171,9 +171,9 @@ pub async fn try_bot_webhook(state: &AppState, to_fp: &str, message: &[u8]) -> b
|
|||||||
let update = if let Ok(wire) =
|
let update = if let Ok(wire) =
|
||||||
bincode::deserialize::<warzone_protocol::message::WireMessage>(message)
|
bincode::deserialize::<warzone_protocol::message::WireMessage>(message)
|
||||||
{
|
{
|
||||||
wire_message_to_update(&wire, message)
|
wire_message_to_update(&wire, message, &token)
|
||||||
} else if let Ok(bot_msg) = serde_json::from_slice::<serde_json::Value>(message) {
|
} else if let Ok(bot_msg) = serde_json::from_slice::<serde_json::Value>(message) {
|
||||||
bot_json_to_update(&bot_msg)
|
bot_json_to_update(&bot_msg, &token)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -367,8 +367,7 @@ async fn get_me(
|
|||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"result": {
|
"result": {
|
||||||
"id": crate::routes::resolve::fp_to_numeric_id(fp),
|
"id": crate::routes::resolve::fp_to_numeric_id_for_bot(fp, &token),
|
||||||
"id_str": fp,
|
|
||||||
"is_bot": true,
|
"is_bot": true,
|
||||||
"first_name": info["name"],
|
"first_name": info["name"],
|
||||||
"username": info["name"],
|
"username": info["name"],
|
||||||
@@ -423,7 +422,7 @@ async fn get_updates(
|
|||||||
let timeout = params.timeout.unwrap_or(0);
|
let timeout = params.timeout.unwrap_or(0);
|
||||||
|
|
||||||
// Step 1: Migrate raw queue entries into the persistent bot_queue.
|
// Step 1: Migrate raw queue entries into the persistent bot_queue.
|
||||||
migrate_raw_queue(&state, bot_fp);
|
migrate_raw_queue(&state, bot_fp, &token);
|
||||||
|
|
||||||
// Step 2: If offset is provided, delete all acknowledged updates (update_id < offset).
|
// Step 2: If offset is provided, delete all acknowledged updates (update_id < offset).
|
||||||
if let Some(offset) = params.offset {
|
if let Some(offset) = params.offset {
|
||||||
@@ -462,7 +461,7 @@ async fn get_updates(
|
|||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
// Check for newly arrived raw messages.
|
// Check for newly arrived raw messages.
|
||||||
migrate_raw_queue(&state, bot_fp);
|
migrate_raw_queue(&state, bot_fp, &token);
|
||||||
let polled = collect_updates(&state, bot_fp, limit);
|
let polled = collect_updates(&state, bot_fp, limit);
|
||||||
if !polled.is_empty() {
|
if !polled.is_empty() {
|
||||||
return Json(serde_json::json!({
|
return Json(serde_json::json!({
|
||||||
@@ -486,7 +485,7 @@ async fn get_updates(
|
|||||||
///
|
///
|
||||||
/// Each raw entry is converted into a Telegram-style Update JSON object, assigned
|
/// Each raw entry is converted into a Telegram-style Update JSON object, assigned
|
||||||
/// a persistent update_id, and stored. The original raw key is deleted.
|
/// a persistent update_id, and stored. The original raw key is deleted.
|
||||||
fn migrate_raw_queue(state: &AppState, bot_fp: &str) {
|
fn migrate_raw_queue(state: &AppState, bot_fp: &str, bot_token: &str) {
|
||||||
let prefix = format!("queue:{}", bot_fp);
|
let prefix = format!("queue:{}", bot_fp);
|
||||||
let mut keys_to_delete = Vec::new();
|
let mut keys_to_delete = Vec::new();
|
||||||
|
|
||||||
@@ -499,9 +498,9 @@ fn migrate_raw_queue(state: &AppState, bot_fp: &str) {
|
|||||||
let update = if let Ok(wire) =
|
let update = if let Ok(wire) =
|
||||||
bincode::deserialize::<warzone_protocol::message::WireMessage>(&value)
|
bincode::deserialize::<warzone_protocol::message::WireMessage>(&value)
|
||||||
{
|
{
|
||||||
wire_message_to_update(&wire, &value)
|
wire_message_to_update(&wire, &value, bot_token)
|
||||||
} else if let Ok(bot_msg) = serde_json::from_slice::<serde_json::Value>(&value) {
|
} else if let Ok(bot_msg) = serde_json::from_slice::<serde_json::Value>(&value) {
|
||||||
bot_json_to_update(&bot_msg)
|
bot_json_to_update(&bot_msg, bot_token)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -521,6 +520,7 @@ fn migrate_raw_queue(state: &AppState, bot_fp: &str) {
|
|||||||
fn wire_message_to_update(
|
fn wire_message_to_update(
|
||||||
wire: &warzone_protocol::message::WireMessage,
|
wire: &warzone_protocol::message::WireMessage,
|
||||||
raw_bytes: &[u8],
|
raw_bytes: &[u8],
|
||||||
|
bot_token: &str,
|
||||||
) -> Option<serde_json::Value> {
|
) -> Option<serde_json::Value> {
|
||||||
match wire {
|
match wire {
|
||||||
warzone_protocol::message::WireMessage::Message {
|
warzone_protocol::message::WireMessage::Message {
|
||||||
@@ -529,19 +529,17 @@ fn wire_message_to_update(
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let raw_b64 = base64::engine::general_purpose::STANDARD.encode(raw_bytes);
|
let raw_b64 = base64::engine::general_purpose::STANDARD.encode(raw_bytes);
|
||||||
let numeric = crate::routes::resolve::fp_to_numeric_id(sender_fingerprint);
|
let numeric = crate::routes::resolve::fp_to_numeric_id_for_bot(sender_fingerprint, bot_token);
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"message": {
|
"message": {
|
||||||
"message_id": id,
|
"message_id": id,
|
||||||
"from": {
|
"from": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"is_bot": false,
|
"is_bot": false,
|
||||||
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"type": "private",
|
"type": "private",
|
||||||
},
|
},
|
||||||
"date": chrono::Utc::now().timestamp(),
|
"date": chrono::Utc::now().timestamp(),
|
||||||
@@ -556,19 +554,17 @@ fn wire_message_to_update(
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let raw_b64 = base64::engine::general_purpose::STANDARD.encode(raw_bytes);
|
let raw_b64 = base64::engine::general_purpose::STANDARD.encode(raw_bytes);
|
||||||
let numeric = crate::routes::resolve::fp_to_numeric_id(sender_fingerprint);
|
let numeric = crate::routes::resolve::fp_to_numeric_id_for_bot(sender_fingerprint, bot_token);
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"message": {
|
"message": {
|
||||||
"message_id": id,
|
"message_id": id,
|
||||||
"from": {
|
"from": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"is_bot": false,
|
"is_bot": false,
|
||||||
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"type": "private",
|
"type": "private",
|
||||||
},
|
},
|
||||||
"date": chrono::Utc::now().timestamp(),
|
"date": chrono::Utc::now().timestamp(),
|
||||||
@@ -584,19 +580,17 @@ fn wire_message_to_update(
|
|||||||
payload,
|
payload,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let numeric = crate::routes::resolve::fp_to_numeric_id(sender_fingerprint);
|
let numeric = crate::routes::resolve::fp_to_numeric_id_for_bot(sender_fingerprint, bot_token);
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"message": {
|
"message": {
|
||||||
"message_id": id,
|
"message_id": id,
|
||||||
"from": {
|
"from": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"is_bot": false,
|
"is_bot": false,
|
||||||
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"type": "private",
|
"type": "private",
|
||||||
},
|
},
|
||||||
"date": chrono::Utc::now().timestamp(),
|
"date": chrono::Utc::now().timestamp(),
|
||||||
@@ -615,19 +609,17 @@ fn wire_message_to_update(
|
|||||||
file_size,
|
file_size,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let numeric = crate::routes::resolve::fp_to_numeric_id(sender_fingerprint);
|
let numeric = crate::routes::resolve::fp_to_numeric_id_for_bot(sender_fingerprint, bot_token);
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"message": {
|
"message": {
|
||||||
"message_id": id,
|
"message_id": id,
|
||||||
"from": {
|
"from": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"is_bot": false,
|
"is_bot": false,
|
||||||
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
"first_name": &sender_fingerprint[..sender_fingerprint.len().min(12)],
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": sender_fingerprint,
|
|
||||||
"type": "private",
|
"type": "private",
|
||||||
},
|
},
|
||||||
"date": chrono::Utc::now().timestamp(),
|
"date": chrono::Utc::now().timestamp(),
|
||||||
@@ -645,23 +637,21 @@ fn wire_message_to_update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a plaintext bot JSON message into a Telegram-style update (without update_id).
|
/// Convert a plaintext bot JSON message into a Telegram-style update (without update_id).
|
||||||
fn bot_json_to_update(bot_msg: &serde_json::Value) -> Option<serde_json::Value> {
|
fn bot_json_to_update(bot_msg: &serde_json::Value, bot_token: &str) -> Option<serde_json::Value> {
|
||||||
let msg_type = bot_msg.get("type").and_then(|v| v.as_str())?;
|
let msg_type = bot_msg.get("type").and_then(|v| v.as_str())?;
|
||||||
match msg_type {
|
match msg_type {
|
||||||
"bot_message" => {
|
"bot_message" => {
|
||||||
let from_fp = bot_msg.get("from").and_then(|v| v.as_str()).unwrap_or("");
|
let from_fp = bot_msg.get("from").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
let numeric = crate::routes::resolve::fp_to_numeric_id(from_fp);
|
let numeric = crate::routes::resolve::fp_to_numeric_id_for_bot(from_fp, bot_token);
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"message": {
|
"message": {
|
||||||
"message_id": bot_msg.get("id").and_then(|v| v.as_str()).unwrap_or(""),
|
"message_id": bot_msg.get("id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||||
"from": {
|
"from": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": from_fp,
|
|
||||||
"is_bot": true,
|
"is_bot": true,
|
||||||
},
|
},
|
||||||
"chat": {
|
"chat": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": from_fp,
|
|
||||||
"type": "private",
|
"type": "private",
|
||||||
},
|
},
|
||||||
"date": bot_msg.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0),
|
"date": bot_msg.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||||
@@ -671,13 +661,12 @@ fn bot_json_to_update(bot_msg: &serde_json::Value) -> Option<serde_json::Value>
|
|||||||
}
|
}
|
||||||
"callback_query" => {
|
"callback_query" => {
|
||||||
let from_fp = bot_msg.get("from").and_then(|v| v.as_str()).unwrap_or("");
|
let from_fp = bot_msg.get("from").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
let numeric = crate::routes::resolve::fp_to_numeric_id(from_fp);
|
let numeric = crate::routes::resolve::fp_to_numeric_id_for_bot(from_fp, bot_token);
|
||||||
Some(serde_json::json!({
|
Some(serde_json::json!({
|
||||||
"callback_query": {
|
"callback_query": {
|
||||||
"id": bot_msg.get("id").and_then(|v| v.as_str()).unwrap_or(""),
|
"id": bot_msg.get("id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||||
"from": {
|
"from": {
|
||||||
"id": numeric,
|
"id": numeric,
|
||||||
"id_str": from_fp,
|
|
||||||
"is_bot": false,
|
"is_bot": false,
|
||||||
},
|
},
|
||||||
"data": bot_msg.get("data").and_then(|v| v.as_str()).unwrap_or(""),
|
"data": bot_msg.get("data").and_then(|v| v.as_str()).unwrap_or(""),
|
||||||
|
|||||||
@@ -7,7 +7,22 @@ use axum::{
|
|||||||
use crate::errors::AppResult;
|
use crate::errors::AppResult;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Convert a fingerprint hex string to a stable i64 ID (for Telegram compatibility).
|
/// Convert a fingerprint to a per-bot unique numeric ID.
|
||||||
|
/// Hash(bot_token + user_fp) → i64. Different bots see different IDs for the same user.
|
||||||
|
/// This prevents cross-bot user correlation (same privacy model as Telegram).
|
||||||
|
pub fn fp_to_numeric_id_for_bot(fp: &str, bot_token: &str) -> i64 {
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(bot_token.as_bytes());
|
||||||
|
hasher.update(b":");
|
||||||
|
hasher.update(fp.as_bytes());
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let mut arr = [0u8; 8];
|
||||||
|
arr.copy_from_slice(&hash[..8]);
|
||||||
|
(i64::from_be_bytes(arr) & 0x7FFFFFFFFFFFFFFF) // ensure positive
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a fingerprint hex string to a stable i64 ID (non-bot contexts).
|
||||||
/// Uses first 8 bytes of the fingerprint as a positive i64.
|
/// Uses first 8 bytes of the fingerprint as a positive i64.
|
||||||
pub fn fp_to_numeric_id(fp: &str) -> i64 {
|
pub fn fp_to_numeric_id(fp: &str) -> i64 {
|
||||||
let clean: String = fp.chars().filter(|c| c.is_ascii_hexdigit()).take(16).collect();
|
let clean: String = fp.chars().filter(|c| c.is_ascii_hexdigit()).take(16).collect();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn service_worker() -> impl IntoResponse {
|
async fn service_worker() -> impl IntoResponse {
|
||||||
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||||
const CACHE = 'wz-v12';
|
const CACHE = 'wz-v13';
|
||||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||||
|
|
||||||
self.addEventListener('install', e => {
|
self.addEventListener('install', e => {
|
||||||
@@ -251,7 +251,7 @@ let pollTimer = null;
|
|||||||
let ws = null; // WebSocket connection
|
let ws = null; // WebSocket connection
|
||||||
let wasmReady = false;
|
let wasmReady = false;
|
||||||
|
|
||||||
const VERSION = '0.0.30';
|
const VERSION = '0.0.31';
|
||||||
let DEBUG = true; // toggle with /debug command
|
let DEBUG = true; // toggle with /debug command
|
||||||
|
|
||||||
// ── Receipt tracking ──
|
// ── Receipt tracking ──
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ Response:
|
|||||||
{"ok":true,"result":[
|
{"ok":true,"result":[
|
||||||
{"update_id":1,"message":{
|
{"update_id":1,"message":{
|
||||||
"message_id":"uuid",
|
"message_id":"uuid",
|
||||||
"from":{"id":123456,"id_str":"sender_fp_hex","is_bot":false},
|
"from":{"id":123456,"is_bot":false},
|
||||||
"chat":{"id":123456,"id_str":"sender_fp_hex","type":"private"},
|
"chat":{"id":123456,"type":"private"},
|
||||||
"date":1711612800,
|
"date":1711612800,
|
||||||
"text":"Hello bot!"
|
"text":"Hello bot!"
|
||||||
}}
|
}}
|
||||||
@@ -69,8 +69,8 @@ Response:
|
|||||||
- `offset` — skip updates < offset (acknowledge processed). **Always use this.**
|
- `offset` — skip updates < offset (acknowledge processed). **Always use this.**
|
||||||
- `timeout` — long-poll seconds (max 50, matches Telegram)
|
- `timeout` — long-poll seconds (max 50, matches Telegram)
|
||||||
- `limit` — max updates (default 100)
|
- `limit` — max updates (default 100)
|
||||||
- `from.id` — numeric (i64 hash of fingerprint, for TG library compat)
|
- `from.id` — numeric (per-bot unique hash, different bots see different IDs for same user)
|
||||||
- `from.id_str` — hex fingerprint string
|
- No raw fingerprint exposed to bots (privacy: bots can't correlate users cross-bot)
|
||||||
|
|
||||||
### sendMessage
|
### sendMessage
|
||||||
```
|
```
|
||||||
@@ -246,7 +246,7 @@ The bridge translates numeric chat_id ↔ fingerprints automatically.
|
|||||||
| User→bot messages | plaintext | plaintext (auto-detected by client) |
|
| User→bot messages | plaintext | plaintext (auto-detected by client) |
|
||||||
| Bot creation | @BotFather chat | @botfather chat (same flow) |
|
| Bot creation | @BotFather chat | @botfather chat (same flow) |
|
||||||
| getUpdates timeout | up to 50s | up to 50s |
|
| getUpdates timeout | up to 50s | up to 50s |
|
||||||
| from.id | integer | integer (hash of fp) + id_str (hex fp) |
|
| from.id | integer | integer (per-bot unique hash, no raw fp exposed) |
|
||||||
| File upload | multipart | JSON reference (v1) |
|
| File upload | multipart | JSON reference (v1) |
|
||||||
| Inline keyboards | full | stored + delivered, no popup |
|
| Inline keyboards | full | stored + delivered, no popup |
|
||||||
| Webhooks | HTTPS POST | HTTP POST (delivered live) |
|
| Webhooks | HTTPS POST | HTTP POST (delivered live) |
|
||||||
@@ -256,7 +256,7 @@ The bridge translates numeric chat_id ↔ fingerprints automatically.
|
|||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
1. **Always use offset** in getUpdates — without it you reprocess messages
|
1. **Always use offset** in getUpdates — without it you reprocess messages
|
||||||
2. **chat_id** — use `msg.chat.id` (numeric) or `msg.chat.id_str` (hex fingerprint)
|
2. **chat_id** — use `msg.chat.id` (numeric, per-bot unique) for replies
|
||||||
3. **Bot names** must end with `bot`, `Bot`, or `_bot`
|
3. **Bot names** must end with `bot`, `Bot`, or `_bot`
|
||||||
4. **Only @botfather** can create bots — direct API registration requires botfather_token
|
4. **Only @botfather** can create bots — direct API registration requires botfather_token
|
||||||
5. **Server needs --enable-bots** — without it all bot endpoints return 403
|
5. **Server needs --enable-bots** — without it all bot endpoints return 403
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ Bots can optionally participate in E2E encryption by registering with a seed and
|
|||||||
- Webhooks: updates are delivered live to the registered URL (POST with JSON body)
|
- Webhooks: updates are delivered live to the registered URL (POST with JSON body)
|
||||||
- chat_id: accepts hex fingerprint or numeric ID (TG compatibility)
|
- chat_id: accepts hex fingerprint or numeric ID (TG compatibility)
|
||||||
- parse_mode: `HTML` renders basic HTML tags (<b>, <i>, <code>, <a>) in clients
|
- parse_mode: `HTML` renders basic HTML tags (<b>, <i>, <code>, <a>) in clients
|
||||||
- from.id is numeric (integer), from.id_str contains the hex fingerprint
|
- from.id is per-bot unique numeric (bots can't correlate users cross-bot, no raw fingerprint exposed)
|
||||||
|
|
||||||
Update types in getUpdates:
|
Update types in getUpdates:
|
||||||
- Encrypted msg: text=null, raw_encrypted=base64
|
- Encrypted msg: text=null, raw_encrypted=base64
|
||||||
|
|||||||
Reference in New Issue
Block a user