Files
EugeneTes 92a0061fdd feat(telegram): ADMIN_USER_ID may /impersonate another user
While on, the admin's updates are routed as the target user's (allow-list,
chat, workspace, session, settings, jobs) and the target chat's output goes
to the admin's chat. The target's own updates are dropped with a notice that
an admin is working with their account. In memory; /impersonate 0 stops.
2026-10-05 07:24:07 +00:00

48 lines
2.9 KiB
C#
Raw Permalink 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.Globalization;
namespace Volcanic.AgenticCore.Telegram.Chats;
/// <summary>
/// The admin's (<see cref="TelegramOptions.AdminUserId"/>) <c>/impersonate &lt;user_id&gt;</c>. While it is on, the 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 the
/// 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.
/// Kept in memory: a restart ends it.
/// </summary>
public sealed class Impersonation(TelegramOptions options)
{
/// <summary>The reply to the impersonated user while the admin works as them.</summary>
public const string BlockedNotice = "šŸ”’ An admin is working with your account right now. Please try again later.";
private long _target; // 0 = off
public long? AdminId => options.AdminUserId;
/// <summary>The impersonated user, or null when off.</summary>
public long? Target => Volatile.Read(ref _target) is var target and not 0 ? target : null;
public bool IsAdmin(long? userId) => userId is { } id && id == AdminId;
/// <summary>True for the impersonated user themselves: their updates are blocked while the admin works as them.</summary>
public bool IsBlocked(long? userId) => userId is { } id && Target == id;
/// <summary>The user (or private chat) id the bot acts for: the target for the admin while impersonating, else <paramref name="id"/>.</summary>
public long EffectiveId(long id) => IsAdmin(id) && Target is { } target ? target : id;
/// <summary>Where chat <paramref name="chatId"/>'s output goes: the admin's chat while the admin impersonates it.</summary>
public long DeliveryChatFor(long chatId) => Target == chatId && AdminId is { } admin ? admin : chatId;
/// <summary>Handle <c>/impersonate</c>'s argument and return the reply for the admin.</summary>
public string Apply(string arg)
{
if (!long.TryParse(arg.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var id))
return "Usage: /impersonate <user_id> (0 stops). " + (Target is { } current ? $"You are acting as user {current}." : "You are not impersonating anyone.");
if (id == 0 || IsAdmin(id))
return Interlocked.Exchange(ref _target, 0) is not 0 ? "šŸŽ­ Impersonation off: you are yourself again." : "You are not impersonating anyone.";
Volatile.Write(ref _target, 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.";
}
}