4b0f328f2e
- Add standalone C# console runner tests/e2e/runner/ using WTelegramClient - Provide TelegramUserClient wrapper: login, create supergroup, invite bot, send messages/commands, read recent messages, wait for bot reply - Add .env.example and runner .gitignore to keep secrets/session files out of git - Update E2E README with runner instructions and status table - Runner project intentionally excluded from GM-Relay.slnx to avoid CI/AOT impact Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
4.2 KiB
C#
115 lines
4.2 KiB
C#
using TL;
|
|
using WTelegram;
|
|
|
|
namespace GmRelay.E2E.Runner;
|
|
|
|
public sealed class TelegramUserClient : IDisposable
|
|
{
|
|
private readonly Client _client;
|
|
private readonly RunnerConfig _config;
|
|
|
|
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)
|
|
{
|
|
// Creating a megagroup gives us an UpdatesBase with Chats immediately.
|
|
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.");
|
|
|
|
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 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?> 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 void Dispose()
|
|
{
|
|
_client.Dispose();
|
|
}
|
|
|
|
private async Task<Channel> ResolveChannelAsync(long channelId, CancellationToken cancellationToken = default)
|
|
{
|
|
var dialogs = await _client.Messages_GetAllDialogs();
|
|
var channel = dialogs.chats.Values
|
|
.OfType<Channel>()
|
|
.FirstOrDefault(c => c.id == channelId);
|
|
|
|
return channel ?? throw new InvalidOperationException($"Could not resolve channel {channelId}.");
|
|
}
|
|
}
|
|
|
|
public sealed record ChatGroup(long Id, string Title);
|