mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-29 22:22:35 +00:00
docs accuracy
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user