Files
GmRelayBot/tests/e2e/runner/TelegramUserClient.cs
T
Toutsu 892f39401c feat(e2e): #148 /newsession scenario from creation to publication
- Add NewSessionScenario that walks the Telegram wizard:
  single game, title, skip description/cover, D&D 5e, 4h, datetime,
  capacity, online format, join link, public visibility, publish, confirm
- Add ClickInlineButtonAsync / ClickInlineButtonByTextAsync to TelegramUserClient
- Add local WizardCallback/Step constants mirroring GmRelay.Shared wizard wire format
- Program.cs now runs full flow: group setup + /newsession + cleanup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:22:10 +03:00

181 lines
6.9 KiB
C#

using System.Collections.Concurrent;
using System.Text;
using TL;
using WTelegram;
namespace GmRelay.E2E.Runner;
public sealed class TelegramUserClient : IDisposable
{
private readonly Client _client;
private readonly RunnerConfig _config;
private readonly ConcurrentDictionary<long, Channel> _knownChannels = new();
public TelegramUserClient(RunnerConfig config)
{
_config = config;
_client = new Client(_ => Environment.GetEnvironmentVariable(_));
}
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
var me = await _client.LoginUserIfNeeded();
Console.WriteLine($"Logged in as {me.first_name} {me.last_name} (id={me.id})");
}
public async Task<ChatGroup> CreateGroupAsync(string title, string about = "", CancellationToken cancellationToken = default)
{
var updates = await _client.Channels_CreateChannel(title, about, megagroup: true);
var channel = updates.Chats.Values.OfType<Channel>().FirstOrDefault()
?? throw new InvalidOperationException("Failed to create a supergroup.");
_knownChannels[channel.id] = channel;
return new ChatGroup(channel.id, channel.title);
}
public async Task InviteBotToGroupAsync(ChatGroup group, string botUsername, CancellationToken cancellationToken = default)
{
if (botUsername.StartsWith('@'))
botUsername = botUsername[1..];
var resolved = await _client.Contacts_ResolveUsername(botUsername);
if (resolved.User is null)
throw new InvalidOperationException($"Could not resolve bot @{botUsername}.");
var channel = await ResolveChannelAsync(group.Id, cancellationToken);
var inputUser = new InputUser(resolved.User.id, resolved.User.access_hash);
await _client.Channels_InviteToChannel(channel, new InputUserBase[] { inputUser });
}
public async Task DeleteGroupAsync(ChatGroup group, CancellationToken cancellationToken = default)
{
var channel = await ResolveChannelAsync(group.Id, cancellationToken);
await _client.Channels_DeleteChannel(channel);
_knownChannels.TryRemove(group.Id, out _);
}
public async Task SendMessageAsync(ChatGroup group, string text, CancellationToken cancellationToken = default)
{
var channel = await ResolveChannelAsync(group.Id, cancellationToken);
await _client.SendMessageAsync(channel, text);
}
public async Task<IReadOnlyList<Message>> GetRecentMessagesAsync(ChatGroup group, int limit = 30, CancellationToken cancellationToken = default)
{
var channel = await ResolveChannelAsync(group.Id, cancellationToken);
var history = await _client.Messages_GetHistory(channel, limit: limit);
return history.Messages
.OfType<Message>()
.OrderByDescending(m => m.Date)
.ToList();
}
public async Task<Message?> GetLatestBotMessageAsync(ChatGroup group, CancellationToken cancellationToken = default)
{
var messages = await GetRecentMessagesAsync(group, limit: 20, cancellationToken);
return messages.FirstOrDefault(m => m.from_id is not PeerUser userPeer || userPeer.user_id != _client.UserId);
}
public async Task<Message?> WaitForBotReplyAsync(
ChatGroup group,
string? containsText = null,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default)
{
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(30));
while (DateTime.UtcNow < deadline)
{
cancellationToken.ThrowIfCancellationRequested();
var messages = await GetRecentMessagesAsync(group, limit: 50, cancellationToken);
var match = messages.FirstOrDefault(m =>
m.from_id is not PeerUser userPeer || userPeer.user_id != _client.UserId);
if (match is not null)
{
if (containsText is null || (match.message?.Contains(containsText, StringComparison.OrdinalIgnoreCase) ?? false))
return match;
}
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
return null;
}
public async Task SendCommandAsync(ChatGroup group, string command, CancellationToken cancellationToken = default)
{
if (!command.StartsWith('/'))
command = "/" + command;
await SendMessageAsync(group, command, cancellationToken);
}
public async Task ClickInlineButtonAsync(
ChatGroup group,
string callbackData,
int? messageId = null,
CancellationToken cancellationToken = default)
{
var channel = await ResolveChannelAsync(group.Id, cancellationToken);
var targetMessageId = messageId ?? (await GetLatestBotMessageAsync(group, cancellationToken))?.id
?? throw new InvalidOperationException("No bot message found to click.");
await _client.Messages_GetBotCallbackAnswer(
channel,
targetMessageId,
Encoding.UTF8.GetBytes(callbackData));
}
public async Task ClickInlineButtonByTextAsync(
ChatGroup group,
string buttonTextContains,
int? messageId = null,
CancellationToken cancellationToken = default)
{
var message = messageId.HasValue
? (await GetRecentMessagesAsync(group, limit: 20, cancellationToken)).FirstOrDefault(m => m.id == messageId.Value)
: await GetLatestBotMessageAsync(group, cancellationToken);
if (message is null)
throw new InvalidOperationException("No bot message found to click.");
if (message.reply_markup is not ReplyInlineMarkup markup)
throw new InvalidOperationException("Latest bot message has no inline keyboard.");
var button = markup.rows
.SelectMany(r => r.buttons)
.OfType<KeyboardButtonCallback>()
.FirstOrDefault(b => b.text.Contains(buttonTextContains, StringComparison.OrdinalIgnoreCase));
if (button is null)
throw new InvalidOperationException($"No inline button matching '{buttonTextContains}' found.");
await ClickInlineButtonAsync(group, Encoding.UTF8.GetString(button.data), message.id, cancellationToken);
}
public void Dispose()
{
_client.Dispose();
}
private async Task<Channel> ResolveChannelAsync(long channelId, CancellationToken cancellationToken = default)
{
if (_knownChannels.TryGetValue(channelId, out var cached))
return cached;
var dialogs = await _client.Messages_GetAllDialogs();
var channel = dialogs.chats.Values
.OfType<Channel>()
.FirstOrDefault(c => c.id == channelId);
if (channel is null)
throw new InvalidOperationException($"Could not resolve channel {channelId}.");
_knownChannels[channelId] = channel;
return channel;
}
}
public sealed record ChatGroup(long Id, string Title);