mirror of
https://github.com/MrFadiAi/openclaw-manager.git
synced 2026-08-14 00:57:59 +00:00
feat: per-account DM allowlist with primary bot inheritance
- Add allow_from field to TelegramAccount struct and frontend interface - Read/write allowFrom per-account in get/save_telegram_accounts - Auto-inherit allowFrom from primary bot when creating new accounts - Per-account DM Users UI in expanded bot settings (add/remove user IDs) - Pre-populate new accounts with primary bot's allowFrom list
This commit is contained in:
@@ -1626,6 +1626,8 @@ pub struct TelegramAccount {
|
||||
pub exclusive_topics: Option<Vec<String>>,
|
||||
pub groups: Option<serde_json::Value>,
|
||||
pub primary: Option<bool>,
|
||||
#[serde(alias = "allowFrom", alias = "allow_from")]
|
||||
pub allow_from: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Get all Telegram bot accounts
|
||||
@@ -1668,6 +1670,13 @@ pub async fn get_telegram_accounts() -> Result<Vec<TelegramAccount>, String> {
|
||||
},
|
||||
groups: acct_val.get("groups").cloned(),
|
||||
primary: None, // Will be set below
|
||||
allow_from: acct_val.get("allowFrom")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| {
|
||||
if let Some(s) = v.as_str() { Some(s.to_string()) }
|
||||
else if let Some(n) = v.as_i64() { Some(n.to_string()) }
|
||||
else { None }
|
||||
}).collect()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1685,6 +1694,13 @@ pub async fn get_telegram_accounts() -> Result<Vec<TelegramAccount>, String> {
|
||||
exclusive_topics: None,
|
||||
groups: config.pointer("/channels/telegram/groups").cloned(),
|
||||
primary: None,
|
||||
allow_from: config.pointer("/channels/telegram/allowFrom")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| {
|
||||
if let Some(s) = v.as_str() { Some(s.to_string()) }
|
||||
else if let Some(n) = v.as_i64() { Some(n.to_string()) }
|
||||
else { None }
|
||||
}).collect()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1769,6 +1785,32 @@ pub async fn save_telegram_account(account: TelegramAccount) -> Result<String, S
|
||||
// Fix for validation error: dmPolicy="open" requires allowFrom to include "*"
|
||||
if dp == "open" {
|
||||
acct_obj["allowFrom"] = json!(["*"]);
|
||||
} else if let Some(ref af) = account.allow_from {
|
||||
if !af.is_empty() {
|
||||
// Convert string IDs to numbers where possible for Core compatibility
|
||||
let allow_vals: Vec<serde_json::Value> = af.iter().map(|id| {
|
||||
if let Ok(n) = id.parse::<i64>() { json!(n) } else { json!(id) }
|
||||
}).collect();
|
||||
acct_obj["allowFrom"] = json!(allow_vals);
|
||||
}
|
||||
} else {
|
||||
// Auto-inherit from primary bot if no explicit allow_from provided
|
||||
let primary_id = load_manager_config()
|
||||
.unwrap_or(json!({}))
|
||||
.pointer("/primaryBotAccount")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
if let Some(pid) = primary_id {
|
||||
if pid != account.id {
|
||||
// Read primary account's allowFrom
|
||||
if let Some(primary_allow) = config.pointer(&format!("/channels/telegram/accounts/{}/allowFrom", pid))
|
||||
.and_then(|v| v.as_array()) {
|
||||
if !primary_allow.is_empty() && primary_allow.iter().any(|v| v.as_str() != Some("*")) {
|
||||
acct_obj["allowFrom"] = json!(primary_allow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(sm) = &account.stream_mode {
|
||||
|
||||
@@ -252,6 +252,7 @@ export function Channels() {
|
||||
exclusive_topics?: string[];
|
||||
groups?: Record<string, unknown>;
|
||||
primary?: boolean;
|
||||
allow_from?: string[];
|
||||
}
|
||||
const [telegramAccounts, setTelegramAccounts] = useState<TelegramAccountInfo[]>([]);
|
||||
const [showAddAccountDialog, setShowAddAccountDialog] = useState(false);
|
||||
@@ -1350,6 +1351,69 @@ export function Channels() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Per-account DM Allowed Users */}
|
||||
{(acct.dm_policy === 'pairing' || acct.dm_policy === 'allowlist') && (() => {
|
||||
const dmUsers = acct.allow_from || [];
|
||||
const updateDmUsers = (newList: string[]) => {
|
||||
const updated = telegramAccounts.map(a => a.id === acct.id ? { ...a, allow_from: newList } : a);
|
||||
setTelegramAccounts(updated);
|
||||
};
|
||||
return (
|
||||
<div className="p-3 bg-dark-600 rounded-lg border border-dark-500 space-y-2">
|
||||
<label className="text-xs text-gray-400 font-semibold">Allowed DM Users (User ID)</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. 123456789"
|
||||
className="input-base text-xs flex-1"
|
||||
id={`dm-user-${acct.id}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
if (val && !dmUsers.includes(val)) {
|
||||
updateDmUsers([...dmUsers, val]);
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const input = document.getElementById(`dm-user-${acct.id}`) as HTMLInputElement;
|
||||
const val = input?.value.trim();
|
||||
if (val && !dmUsers.includes(val)) {
|
||||
updateDmUsers([...dmUsers, val]);
|
||||
input.value = '';
|
||||
}
|
||||
}}
|
||||
className="btn-secondary p-1.5"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{dmUsers.map(id => (
|
||||
<div key={id} className="flex items-center justify-between text-xs bg-dark-500 px-2.5 py-1 rounded-lg border border-dark-400">
|
||||
<span className="font-mono text-gray-300">{id}</span>
|
||||
<button
|
||||
onClick={() => updateDmUsers(dmUsers.filter(u => u !== id))}
|
||||
className="text-gray-500 hover:text-red-400"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{dmUsers.length === 0 && (
|
||||
<p className="text-[10px] text-gray-500 italic text-center py-1">
|
||||
{acct.dm_policy === 'pairing' ? 'Users added via pairing flow. You can also add manually.' : 'No users allowed. Add user IDs above.'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500">Saved per-account as <code className="px-1 py-0.5 bg-dark-500 rounded">allowFrom</code>. Inherited from primary bot if empty.</p>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-dark-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -1412,7 +1476,10 @@ export function Channels() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (newAccountId && newAccountToken) {
|
||||
await handleSaveAccount({ id: newAccountId, bot_token: newAccountToken });
|
||||
// Pre-populate allow_from from primary bot
|
||||
const primaryBot = telegramAccounts.find(a => a.primary);
|
||||
const inheritedAllowFrom = primaryBot?.allow_from?.filter(id => id !== '*');
|
||||
await handleSaveAccount({ id: newAccountId, bot_token: newAccountToken, allow_from: inheritedAllowFrom && inheritedAllowFrom.length > 0 ? inheritedAllowFrom : undefined });
|
||||
setNewAccountId('');
|
||||
setNewAccountToken('');
|
||||
setShowAddAccountDialog(false);
|
||||
|
||||
Reference in New Issue
Block a user