247 lines
11 KiB
C#
247 lines
11 KiB
C#
using Microsoft.Extensions.Hosting;
|
|
using Telegram.Bot.Types;
|
|
using Telegram.Bot.Types.Enums;
|
|
|
|
namespace Volcanic.AgenticCore.Telegram.Tests.Support;
|
|
|
|
public static class Updates
|
|
{
|
|
public const long AllowedUser = 1001;
|
|
public const long Stranger = 666;
|
|
|
|
private static int _nextUpdateId;
|
|
private static int _nextMessageId = 1;
|
|
|
|
public static Update Text(string text, long userId = AllowedUser, long? chatId = null, ChatType chatType = ChatType.Private) => new()
|
|
{
|
|
Id = Interlocked.Increment(ref _nextUpdateId),
|
|
Message = new Message
|
|
{
|
|
Id = Interlocked.Increment(ref _nextMessageId),
|
|
From = new User { Id = userId, FirstName = "Test" },
|
|
Chat = new Chat { Id = chatId ?? userId, Type = chatType },
|
|
Text = text,
|
|
Date = DateTime.UtcNow,
|
|
},
|
|
};
|
|
|
|
public static Update Photo(string? caption = null, long userId = AllowedUser) => new()
|
|
{
|
|
Id = Interlocked.Increment(ref _nextUpdateId),
|
|
Message = new Message
|
|
{
|
|
Id = Interlocked.Increment(ref _nextMessageId),
|
|
From = new User { Id = userId, FirstName = "Test" },
|
|
Chat = new Chat { Id = userId, Type = ChatType.Private },
|
|
Photo = [new PhotoSize { FileId = "photo", FileUniqueId = "photo-u", Width = 10, Height = 10 }],
|
|
Caption = caption,
|
|
Date = DateTime.UtcNow,
|
|
},
|
|
};
|
|
|
|
public const long GroupId = -1001234567890;
|
|
|
|
/// <summary>A text message in a group (a supergroup unless <paramref name="type"/> says otherwise).</summary>
|
|
public static Update GroupText(string text, long userId = Stranger, long groupId = GroupId, string title = "Dev Team", ChatType type = ChatType.Supergroup,
|
|
string firstName = "Alice") => new()
|
|
{
|
|
Id = Interlocked.Increment(ref _nextUpdateId),
|
|
Message = new Message
|
|
{
|
|
Id = Interlocked.Increment(ref _nextMessageId),
|
|
From = new User { Id = userId, FirstName = firstName },
|
|
Chat = new Chat { Id = groupId, Type = type, Title = title },
|
|
Text = text,
|
|
Date = DateTime.UtcNow,
|
|
},
|
|
};
|
|
|
|
/// <summary>The bot's own membership in a group changed (added by <paramref name="byUser"/>, or removed).</summary>
|
|
public static Update BotMembership(ChatMemberStatus status, long byUser = AllowedUser, long groupId = GroupId, string title = "Dev Team") => new()
|
|
{
|
|
Id = Interlocked.Increment(ref _nextUpdateId),
|
|
MyChatMember = new ChatMemberUpdated
|
|
{
|
|
Chat = new Chat { Id = groupId, Type = ChatType.Supergroup, Title = title },
|
|
From = new User { Id = byUser, FirstName = "Adder" },
|
|
Date = DateTime.UtcNow,
|
|
OldChatMember = new ChatMemberLeft { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
|
|
NewChatMember = status switch
|
|
{
|
|
ChatMemberStatus.Administrator => new ChatMemberAdministrator { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
|
|
ChatMemberStatus.Kicked => new ChatMemberBanned { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
|
|
ChatMemberStatus.Left => new ChatMemberLeft { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
|
|
_ => new ChatMemberMember { User = new User { Id = BotUserId, IsBot = true, FirstName = "Fake" } },
|
|
},
|
|
},
|
|
};
|
|
|
|
public static Update WithId(int id) => new() { Id = id, Message = Text("#" + id).Message };
|
|
|
|
/// <summary>The fake bot's user id (<see cref="FakeBotClient.BotId"/>).</summary>
|
|
public const long BotUserId = 42;
|
|
|
|
private static Message NewMessage(int? id, long userId = AllowedUser) => new()
|
|
{
|
|
Id = id ?? Interlocked.Increment(ref _nextMessageId),
|
|
From = new User { Id = userId, FirstName = "Test" },
|
|
Chat = new Chat { Id = userId, Type = ChatType.Private },
|
|
Date = DateTime.UtcNow,
|
|
};
|
|
|
|
private static Update Wrap(Message message) => new() { Id = Interlocked.Increment(ref _nextUpdateId), Message = message };
|
|
|
|
/// <summary>An earlier message sent by the bot, for <see cref="Reply"/> and <see cref="Quote"/>.</summary>
|
|
public static Message BotMessage(string text, int? id = null)
|
|
{
|
|
var m = NewMessage(id);
|
|
m.From = new User { Id = BotUserId, IsBot = true, FirstName = "Fake", Username = "fake_bot" };
|
|
m.Text = text;
|
|
return m;
|
|
}
|
|
|
|
/// <summary>A text message replying to <paramref name="replyTo"/>.</summary>
|
|
public static Update Reply(string text, Message replyTo, int? id = null)
|
|
{
|
|
var m = NewMessage(id);
|
|
m.Text = text;
|
|
m.ReplyToMessage = replyTo;
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>A text message replying to <paramref name="replyTo"/> with a selected <paramref name="quote"/>.</summary>
|
|
public static Update Quote(string text, Message replyTo, string quote, int? id = null)
|
|
{
|
|
var update = Reply(text, replyTo, id);
|
|
update.Message!.Quote = new TextQuote { Text = quote, Position = 0 };
|
|
return update;
|
|
}
|
|
|
|
/// <summary>A text message replying to a message from another chat (Telegram's "reply in another chat").</summary>
|
|
public static Update ExternalReply(string text, MessageOrigin origin, string? quote = null, int? id = null)
|
|
{
|
|
var m = NewMessage(id);
|
|
m.Text = text;
|
|
m.ExternalReply = new ExternalReplyInfo { Origin = origin };
|
|
if (quote is not null) m.Quote = new TextQuote { Text = quote, Position = 0 };
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>A forwarded text message.</summary>
|
|
public static Update Forward(string text, MessageOrigin origin, int? id = null)
|
|
{
|
|
var m = NewMessage(id);
|
|
m.Text = text;
|
|
m.ForwardOrigin = origin;
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>One photo of an album (media group <paramref name="groupId"/>), with its own file id.</summary>
|
|
public static Update AlbumPhoto(string groupId, string? caption = null, MessageOrigin? forwardedFrom = null, int? id = null)
|
|
{
|
|
var m = NewMessage(id);
|
|
m.MediaGroupId = groupId;
|
|
m.Photo = [new PhotoSize { FileId = $"photo-{m.Id}", FileUniqueId = $"photo-{m.Id}-u", Width = 10, Height = 10 }];
|
|
m.Caption = caption;
|
|
m.ForwardOrigin = forwardedFrom;
|
|
return Wrap(m);
|
|
}
|
|
|
|
public static Update StickerMessage(string emoji = "👍")
|
|
{
|
|
var m = NewMessage(null);
|
|
m.Sticker = new Sticker { FileId = "sticker", FileUniqueId = "sticker-u", Type = StickerType.Regular, Width = 1, Height = 1, Emoji = emoji };
|
|
return Wrap(m);
|
|
}
|
|
|
|
// Media helpers end in "Message": a member named Document/Video/Voice/Audio would hide the Telegram type in this class.
|
|
|
|
/// <summary>A document (file id <paramref name="fileId"/>, scripted in <see cref="FakeBotClient.Files"/>).</summary>
|
|
public static Update DocumentMessage(string? fileName, string? mimeType, long? size, string? caption = null, string fileId = "doc")
|
|
{
|
|
var m = NewMessage(null);
|
|
m.Document = new Document { FileId = fileId, FileUniqueId = fileId + "-u", FileName = fileName, MimeType = mimeType, FileSize = size };
|
|
m.Caption = caption;
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>A video (MIME <c>video/mp4</c>).</summary>
|
|
public static Update VideoMessage(string? fileName, long? size, string? caption = null, string fileId = "video")
|
|
{
|
|
var m = NewMessage(null);
|
|
m.Video = new Video { FileId = fileId, FileUniqueId = fileId + "-u", Width = 1, Height = 1, Duration = 1, FileName = fileName, MimeType = "video/mp4", FileSize = size };
|
|
m.Caption = caption;
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>A voice note (MIME <c>audio/ogg</c>).</summary>
|
|
public static Update VoiceMessage(long? size = 3, string? caption = null, string fileId = "voice")
|
|
{
|
|
var m = NewMessage(null);
|
|
m.Voice = new Voice { FileId = fileId, FileUniqueId = fileId + "-u", Duration = 2, MimeType = "audio/ogg", FileSize = size };
|
|
m.Caption = caption;
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>An audio file.</summary>
|
|
public static Update AudioMessage(string? fileName, string? mimeType, long? size, string? caption = null, string fileId = "audio")
|
|
{
|
|
var m = NewMessage(null);
|
|
m.Audio = new Audio { FileId = fileId, FileUniqueId = fileId + "-u", Duration = 60, FileName = fileName, MimeType = mimeType, FileSize = size };
|
|
m.Caption = caption;
|
|
return Wrap(m);
|
|
}
|
|
|
|
/// <summary>A channel post origin, optionally signed by its author.</summary>
|
|
public static MessageOriginChannel Channel(string title, string? signature = null) => new()
|
|
{
|
|
Chat = new Chat { Id = -100, Type = ChatType.Channel, Title = title },
|
|
MessageId = 1,
|
|
AuthorSignature = signature,
|
|
Date = DateTime.UtcNow,
|
|
};
|
|
|
|
/// <summary>A visible user origin. Named <c>UserOrigin</c> so it doesn't hide the <see cref="User"/> type in this class.</summary>
|
|
public static MessageOriginUser UserOrigin(string username, string first, string? last = null) => new()
|
|
{
|
|
SenderUser = new User { Id = 7, FirstName = first, LastName = last, Username = username },
|
|
Date = DateTime.UtcNow,
|
|
};
|
|
|
|
public static Update Callback(string data, long chatId, long userId = AllowedUser) => new()
|
|
{
|
|
Id = Interlocked.Increment(ref _nextUpdateId),
|
|
CallbackQuery = new CallbackQuery
|
|
{
|
|
Id = Guid.NewGuid().ToString("N"),
|
|
From = new User { Id = userId, FirstName = "Test" },
|
|
Message = new Message { Id = 1, Chat = new Chat { Id = chatId, Type = ChatType.Private }, Date = DateTime.UtcNow },
|
|
Data = data,
|
|
ChatInstance = "ci",
|
|
},
|
|
};
|
|
}
|
|
|
|
public static class Wait
|
|
{
|
|
public static async Task UntilAsync(Func<bool> condition, string what, int timeoutMs = 5000)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
|
|
while (!condition())
|
|
{
|
|
if (DateTime.UtcNow > deadline) throw new TimeoutException($"Timed out waiting for {what}");
|
|
await Task.Delay(10);
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class FakeLifetime : IHostApplicationLifetime
|
|
{
|
|
private readonly CancellationTokenSource _stopping = new();
|
|
public CancellationToken ApplicationStarted => CancellationToken.None;
|
|
public CancellationToken ApplicationStopping => _stopping.Token;
|
|
public CancellationToken ApplicationStopped => CancellationToken.None;
|
|
public bool StopRequested => _stopping.IsCancellationRequested;
|
|
public void StopApplication() => _stopping.Cancel();
|
|
}
|