The simplest way to build Telegram Bot in the whole multiverse 🤖
Sometimes we just want to write code, run it and make it work. And this framework is just about it.
public class MyBot : BotBase
{
public MyBot(IOptions options) : base(options) { }
[Command]
public string Start() => "Hello, I'm bot";
}
...
_ = new MyBot(new DefaultOptions(token)).RunAsync();
Yep, no more
Yep, so simple
Are you interested? Then, let's go!
-
This project is based on Telegram.Bot - the most popular and current wrapper over the Telegram Bot API, so there is a good chance that you're already partially familiar with what is under the hood of this framework
-
.NET Standart 2.1+ (.NET Core 3.0+), cause I don't see this project without using such cool things as
IAsyncEnumerable<T>
(I'll show some use cases later) -
This library uses the Nullable feature, so it will be easier for you to understand where null is a valid value and where it's not
-
Alas, I had no enough time to write documentation for this project yet 😕 I'll fix this in the nearest future, but for now I hope that an example will be enough to bring you up to date
Since the library doesn't have any documentation yet, I'll try to explain the example as intelligibly as possible so that you can understand what is going on here :)
Let's take a look at creating a simple bot step by step at first:
public class TestBot
{
}
There're 2 default base classes available: BotBase
and StateBotBase
. The last one has special State
property, which can be used to store some user-specific data between command calls, that’s the whole difference.
So your class will be look like
public class TestBot : BotBase
{
public TestBot(IOptions options) : base(options) { }
}
or
public class TestBot : StateBotBase
{
public TestBot(IStateOptions options) : base(options) { }
}
By default, any method (static, instance, public or non public) that is tagged with the CommandAttribute
attribute is a command.
public class TestBot : StateBotBase
{
public TestBot(IStateOptions options) : base(options) { }
[Command("start", "begin")]
public string Start() => "Hi, I'm bot";
}
It means that our bot has one command, which can be triggered somehow like this:
/start
/begin
start
begin
StArt 42
BEGIn foo
Command name is case insensitive and it's not required to start it with slash.
I marked my example class with BotAttribute
:
[Bot(Bindings.AnyPublic)]
public class TestBot : StateBotBase
This overrides the behavior of default ICommandExtractor
. Now, as any public method will be tagged as a command, there's no need to use CommandAttribute
anymore, so these lines of code become identical:
public string Start() => ...
[Command]
public string Start() => ...
[Command("Start")]
public string Start() => ...
If you don't want some method to become a command in any cases, just mark it with IgnoreCommandAttribute
:
[IgnoreCommand]
public void NotCommand() => ...
If you want your bot to respond with a prepared message in cases where the user hasn't called any of available commands, just mark one of the methods by DefaultCommandAttribute
:
[DefaultCommand]
public static string Default() => "Sorry, I have no such command";
The value returned by the method processes using one of the IResultProcessors
. Here's the list of types that can be processed by default:
string
ChatAction
AnimationMessage
AudioMessage
ChatActionMessage
ContactMessage
DiceMessage
DocumentMessage
GameMessage
LocationMessage
MediaGroupMessage
PhotoMessage
PollMessage
StickerMessage
TextMessage
VenueMessage
VideoMessage
VideoNoteMessage
VoiceMessage
Any value could be wrapped with Task
, Task<T>
, IEnumerable
, IEnumerable<T>
and IAsyncEnumerable<T>
. So feel free to write asynchronous code and return more then 1 value from the method:
// This command will send 2 messages to its invoker
[Command("values")]
public IEnumerable<string> GetValues()
{
yield return "0";
yield return "1";
}
// This command will send 2 messages with a 1 second break to its invoker
[Command("valuesdelay")]
public async IAsyncEnumerable<string> GetValuesAsync()
{
yield return "0";
await Task.Delay(1000);
yield return "1";
}
// This command will send 1 message with text "Hello!" to the chat with id 1
// (not to its invoker)
[Command("send")]
public async Task<TextMessage> SendAsync()
{
await SomeWorkAsync();
return new TextMessage("Hello!", chatId: 1);
}
public string Sum(int a, int? b = null, int c = 1) => $"Sum of <b>{a}</b>, <b>{b ?? 0}</b> and <b>{c}</b> is {a + (b ?? 0) + c}";
As you can see, there's a possibility to pass parameters to the method by invoking a command.
/sum
Sum of 0, 0 and 1 is 1
/sum 2
Sum of 2, 0 and 1 is 3
/sum 3 5 2
Sum of 3, 5 and 2 is 10
There're some inbuilt special values which can be provided by ISpecialValueProvider
s:
- (
User
anyName) - user who called the command - (
ChatId
anyName) - Id of the chat from which the message came. If chat id isn't available, user id will be returned - (
Update
anyName) -Update
object associated with this command - (
Message
anyName) -Message
object associated with this command - (
ICommand
anyName) - this command info
public string Overload(DateTime? date) => $"Date: {date}";
public string Overload(string begin, string end) => $"Messages: \"{begin}\" & \"{end}\"";
public string Overload(int number) => $"Number: {number}";
public string Overload(string message) => $"Message: \"{message}\"";
The bot is able to determine the most appropriate method's overload for the called command:
/overload 1
Number: 1
/overload 01.01.1970
Date: 01.01.1970 00:00:00
/overload String
Message: "String"
/overload String1 String2
Messages: "String1" & "String2"
/overload String1 String2 String3
Messages: "String1" & "String2"
Overloads are those methods that have the same command names, but they themselves aren't required to bear the same method name, so
these are overloads:
[Command("RyanReynolds")]
public string Deadpool(...) => "Chimichangas";
[Command("RyanReynolds")]
public string WadeWilson(...) => "Chimichangas";
and these are not:
[Command("ChristianBale")]
public string Batman(...) => "I'm the night";
[Command("BenAffleck")]
public string Batman(...) => "I'm the night";
If you define a lot of aliases for a command, there's no need to duplicate them for each overload: it's sufficient that the overloads have just one common name:
[Command(nameof(Overload), "alias0", "alias1")]
public string Overload(...) => ...;
[Command(nameof(Overload), "alias2")]
public string Overload(...) => ...;
[Command]
public string Overload(...) => ...;
public string Overload(...) => ...;
Each of these overloads can be triggered by any of these names:
Overload
alias0
alias1
alias2
Each exception is perceived as a user error, not an error of your code, since, from a semantic point of view, it was the user who could call the command in a wrong way. So these commands
[Command]
public void EnterPrivateZone() => throw new UnauthorizedAccessException("Sorry, you're blacklisted!");
[Command]
public async Task EnterPrivateZoneAsync() => throw new UnauthorizedAccessException("Sorry, you're blacklisted!");
will send an error message to the user who invoked the command.
If you don't like this behavior, you can change it by providing another IResultProcessor
for Exception
objects instead of ExceptionProcessor
BotBase
has some helpful methods for inherited classes:
WaitForUpdateAsync
- The execution of the method will continue only after receiving a response from the user. In case of cancellation requestnull
will be returned (by default there's 10 minutes timeout).WaitForAnswerAsync
- The same asWaitForUpdateAsync
, but it returns only text fromUpdate
object:
public async IAsyncEnumerable<TextMessage> Lucky(User user)
{
const int min = 1;
const int max = 10;
int guessed = new Random().Next(min, max + 1);
yield return "Let's see how lucky you are!";
yield return $@"I've made a number from <b>{min}</b> to <b>{max}</b>. Try to guess it!";
string? userAssumption = await WaitForAnswerAsync(user);
if (string.IsNullOrEmpty(userAssumption))
throw new TimeoutException("Sorry, but you haven't answered for too long. Let's play next time!");
if (userAssumption.Trim() == guessed.ToString())
{
yield return "Lucky guy!";
}
else
{
yield return $@"This time you're out of luck. I figured out the number <b>{guessed}</b>";
}
}
ReplyKeyboard
- GeneratesReplyKeyboardMarkup
by command methods' names:
MainMenu = ReplyKeyboard
(
nameof(Lucky),
nameof(Memento),
nameof(Sum),
nameof(Sticker),
nameof(Help)
);
InlineKeyboard
- GeneratesInlineKeyboardMarkup
by command methods' names
When your bot is ready, it remains just to create an instance of it and call the RunAsync
method :)
string token = "token";
IStateOptions options = new DefaultStateOptions(token);
IBot bot = new TestBot(options);
CancellationTokenSource source = new CancellationTokenSource();
_ = bot.RunAsync(source.Token);
Console.WriteLine("Press any key to stop the bot...");
Console.ReadKey();
source.Cancel();
[ ] Documentation