Files
VolcanicAgenticCore/hosts/Volcanic.AgenticCore.Telegram/Chats/Impersonation.cs
EugeneTes d51438e665 feat(telegram): several admins via ADMIN_USER_IDS, each impersonating independently
ADMIN_USER_IDS (Telegram:AdminUserIds) takes a comma separated list; the older
ADMIN_USER_ID / Telegram:AdminUserId still works, also takes a list and is merged.
Empty entries and duplicates are ignored, non-numeric entries fail validation.
Impersonation is now per admin: one admin per target, an impersonating admin
can't be impersonated, and every admin's chat menu lists /impersonate.
2026-10-15 14:02:59 +00:00

67 lines
4.0 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.Globalization;
namespace Volcanic.AgenticCore.Telegram.Chats;
/// <summary>
/// An admin's (<see cref="TelegramOptions.Admins"/>) <c>/impersonate &lt;user_id&gt;</c>. While it is on, that admin's
/// messages and button presses are handled as the target user's: the allow-list check, the chat, its workspace, session,
/// settings and jobs. Everything the target's chat sends (replies, questions, files, scheduled deliveries) goes to that
/// admin's chat instead, until <c>/impersonate 0</c>. The target user's own messages and button presses are blocked
/// meanwhile and answered with <see cref="BlockedNotice"/>. In private chats the chat id is the user id, so one id maps both.
/// Every admin impersonates independently; a user can be impersonated by one admin at a time, and an admin who is
/// impersonating someone can't be impersonated. Kept in memory: a restart ends it.
/// </summary>
public sealed class Impersonation(TelegramOptions options)
{
/// <summary>The reply to the impersonated user while an admin works as them.</summary>
public const string BlockedNotice = "šŸ”’ An admin is working with your account right now. Please try again later.";
private readonly ConcurrentDictionary<long, long> _targets = new(); // admin → impersonated user
private readonly Lock _gate = new();
public IReadOnlyList<long> AdminIds => options.Admins;
public bool IsAdmin(long? userId) => userId is { } id && options.Admins.Contains(id);
/// <summary>The user <paramref name="adminId"/> impersonates, or null when they don't.</summary>
public long? TargetOf(long adminId) => _targets.TryGetValue(adminId, out var target) ? target : null;
/// <summary>True for an impersonated user themselves: their updates are blocked while an admin works as them.</summary>
public bool IsBlocked(long? userId) => userId is { } id && AdminOf(id) is not null;
/// <summary>The user (or private chat) id the bot acts for: the target for an impersonating admin, else <paramref name="id"/>.</summary>
public long EffectiveId(long id) => TargetOf(id) ?? id;
/// <summary>Where chat <paramref name="chatId"/>'s output goes: the chat of the admin impersonating it, if any.</summary>
public long DeliveryChatFor(long chatId) => AdminOf(chatId) ?? chatId;
private long? AdminOf(long target)
{
foreach (var (admin, current) in _targets)
if (current == target) return admin;
return null;
}
/// <summary>Handle <paramref name="adminId"/>'s <c>/impersonate</c> argument and return the reply for them.</summary>
public string Apply(long adminId, string arg)
{
lock (_gate)
{
if (AdminOf(adminId) is { } other)
return $"Admin {other} is acting as you right now, so you can't impersonate anyone until they stop.";
if (!long.TryParse(arg.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var id))
return "Usage: /impersonate <user_id> (0 stops). " + (TargetOf(adminId) is { } current ? $"You are acting as user {current}." : "You are not impersonating anyone.");
if (id == 0 || id == adminId)
return _targets.TryRemove(adminId, out _) ? "šŸŽ­ Impersonation off: you are yourself again." : "You are not impersonating anyone.";
if (AdminOf(id) is { } taken && taken != adminId)
return $"Admin {taken} is already acting as user {id}. Only one admin can impersonate a user at a time.";
if (TargetOf(id) is not null)
return $"User {id} is an admin who is impersonating someone right now; try again after they stop.";
_targets[adminId] = id;
var reply = $"šŸŽ­ You are now acting as user {id}: the bot handles your messages as theirs, and their chat's output comes here. /impersonate 0 stops.";
return options.IsAllowed(id) ? reply : reply + $"\nāš ļø User {id} is not in the allow-list, so the bot ignores your messages until you stop.";
}
}
}