docs accuracy

This commit is contained in:
jaberjaber23
2026-05-12 14:51:09 +03:00
parent 538e943d3d
commit e31216d5ec
4 changed files with 117 additions and 6 deletions
+12 -3
View File
@@ -1126,15 +1126,24 @@ async fn dispatch_message(
// Prepend sender context so the agent knows who is speaking.
// In group spaces this is essential for multi-user conversations.
//
// For Telegram we also inject the numeric `tg_id` because display names are
// not unique and can change — agents that key per-user state (RBAC, per-user
// workspaces) need a stable identifier. See issue #915.
let sender_name = &message.sender.display_name;
let sender_email = message
.metadata
.get("sender_email")
.and_then(|v| v.as_str());
let telegram_user_id = message
.metadata
.get("telegram_user_id")
.and_then(|v| v.as_str());
let prefixed_text = if !sender_name.is_empty() {
match sender_email {
Some(email) => format!("[From: {sender_name} <{email}>] {text}"),
None => format!("[From: {sender_name}] {text}"),
match (sender_email, telegram_user_id) {
(Some(email), _) => format!("[From: {sender_name} <{email}>] {text}"),
(None, Some(tg_id)) => format!("[From: {sender_name} (tg_id:{tg_id})] {text}"),
(None, None) => format!("[From: {sender_name}] {text}"),
}
} else {
text.clone()
+87
View File
@@ -999,6 +999,16 @@ async fn parse_telegram_update(
// Detect @mention of the bot in entities / caption_entities for MentionOnly group policy.
let mut metadata = HashMap::new();
// Always expose the Telegram numeric user_id in metadata. Display names are not
// unique and can change, so agents that need stable per-user keys (RBAC, per-user
// workspaces, deterministic routing) must rely on this id. The id originates from
// `message.from.id` for normal users or `message.sender_chat.id` for messages sent
// on behalf of a channel/group. See issue #915.
metadata.insert(
"telegram_user_id".to_string(),
serde_json::json!(user_id_str),
);
// Store reply_to_message_id in metadata for downstream consumers.
if let Some(reply_msg) = message.get("reply_to_message") {
if let Some(reply_id) = reply_msg["message_id"].as_i64() {
@@ -1179,6 +1189,83 @@ mod tests {
assert!(matches!(msg.content, ChannelContent::Text(ref t) if t == "Hello, agent!"));
}
#[tokio::test]
async fn test_parse_injects_telegram_user_id_metadata() {
// Issue #915 — agents need a stable per-user identifier. The numeric
// Telegram user_id from `message.from.id` must land in metadata as a
// string so downstream consumers (bridge prompt builder, tools, etc.)
// can key per-user state on it.
let update = serde_json::json!({
"update_id": 555,
"message": {
"message_id": 1,
"from": {
"id": 554772934_i64,
"first_name": "Alena"
},
"chat": {
"id": -1009876543210_i64,
"type": "group"
},
"date": 1700000000,
"text": "Hello"
}
});
let client = test_client();
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None)
.await
.unwrap();
// The chat_id (used for replies) stays on sender.platform_id.
assert_eq!(msg.sender.platform_id, "-1009876543210");
assert_eq!(msg.sender.display_name, "Alena");
// The numeric Telegram user_id is exposed in metadata as a string.
let tg_id = msg
.metadata
.get("telegram_user_id")
.and_then(|v| v.as_str())
.expect("telegram_user_id should be present in metadata");
assert_eq!(tg_id, "554772934");
}
#[tokio::test]
async fn test_parse_sender_chat_user_id_metadata() {
// When a message arrives via `sender_chat` (channel/group posting on
// its own behalf), the chat id is what we have — surface it under
// `telegram_user_id` so the metadata key is always present.
let update = serde_json::json!({
"update_id": 556,
"message": {
"message_id": 2,
"sender_chat": {
"id": -1001234567890_i64,
"type": "channel",
"title": "My Channel"
},
"chat": {
"id": -1001234567890_i64,
"type": "channel"
},
"date": 1700000001,
"text": "Broadcast"
}
});
let client = test_client();
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None)
.await
.unwrap();
let tg_id = msg
.metadata
.get("telegram_user_id")
.and_then(|v| v.as_str())
.expect("telegram_user_id should be present in metadata");
assert_eq!(tg_id, "-1001234567890");
}
#[tokio::test]
async fn test_parse_telegram_command() {
let update = serde_json::json!({
+14
View File
@@ -284,6 +284,20 @@ The Telegram adapter uses long-polling via the `getUpdates` API. It polls every
Messages from authorized users are converted to `ChannelMessage` events and routed to the configured agent. Responses are sent back via the `sendMessage` API. Long responses are automatically split into multiple messages to respect Telegram's 4096-character limit using the shared `split_message()` utility.
### Per-User Identity (`telegram_user_id`)
Every inbound Telegram message exposes the sender's numeric Telegram user_id under `message.metadata["telegram_user_id"]` as a string. This is the stable, permanent identifier from `message.from.id` (or `message.sender_chat.id` for channel/group posts).
Display names are not unique and can change, so agents that need deterministic per-user behavior — RBAC, per-user workspaces, family-assistant style memory keyed by person — should key on `telegram_user_id`, not `sender.display_name`.
The bridge also injects the id into the prompt prefix when no `sender_email` is set:
```
[From: Alena (tg_id:554772934)] Hello!
```
Agents can read the raw metadata field via tool calls that expose `ChannelMessage.metadata` (the prompt prefix is a convenience for plain LLM context). `sender.platform_id` continues to hold the **chat_id** (not user_id), since replies are addressed to the chat, not the user.
### Interactive Setup
```bash
+4 -3
View File
@@ -301,10 +301,11 @@ volumes:
If you prefer mounting `config.toml`, the file must live at `/data/config.toml` inside the container (because of `OPENFANG_HOME=/data`). Placing it at `/opt/openfang/config.toml` or any other path will not be picked up. The port in `api_listen` must also match the port published in `ports:` — the example config in `openfang.toml.example` ships with port `50051` to be safe; change it to `4200` (or whatever port you publish) when running in Docker.
**Security warning.** Once you bind to a non-loopback address, anyone reachable at that address can talk to the API. OpenFang's middleware enforces a fail-closed default:
**Security warning.** Once you bind to a non-loopback address, anyone reachable at that address can talk to the API. OpenFang's middleware enforces a fail-closed default on authenticated routes:
- If `api_key` is empty AND dashboard auth is disabled AND the bind address is not loopback, all non-loopback requests are rejected with `401 Unauthorized`.
- To run in this configuration anyway (not recommended), set `OPENFANG_ALLOW_NO_AUTH=1`. This will be loudly logged.
- If `api_key` is empty AND dashboard auth is disabled AND the bind address is not loopback, authenticated routes reject non-loopback requests with `401 Unauthorized`.
- A small set of public routes (health check, static assets, OAuth callback) remain reachable so the dashboard can render its login page. They do not expose agent data or accept commands.
- To run with full open access anyway (not recommended), set `OPENFANG_ALLOW_NO_AUTH=1`. This will be loudly logged.
The supported ways to expose the dashboard safely: