forked from admin/VolcanicAgenticCore
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.
48 lines
2.9 KiB
C#
48 lines
2.9 KiB
C#
using System.Globalization;
|
||
|
||
namespace Volcanic.AgenticCore.Telegram.Chats;
|
||
|
||
/// <summary>
|
||
/// The admin's (<see cref="TelegramOptions.AdminUserId"/>) <c>/impersonate <user_id></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.";
|
||
}
|
||
}
|