Skip to content

🤖 The simplest way to build Telegram Bot in the whole multiverse

License

Notifications You must be signed in to change notification settings

Kir-Antipov/Termigram

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Termigram

NuGet License

Termigram Logo

The simplest way to build Telegram Bot in the whole multiverse 🤖

Getting Started

Morpheus

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!

Library Details

  • 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

Example

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:

0. Create a new class

public class TestBot
{

}

1. Inherit it from one of the base classes

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) { }
}

2. Create a command

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.

3. BotAttribute

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() => ...

4. IgnoreCommandAttribute

If you don't want some method to become a command in any cases, just mark it with IgnoreCommandAttribute:

[IgnoreCommand]
public void NotCommand() => ...

5. DefaultCommandAttribute

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";

6. Available return types

The value returned by the method processes using one of the IResultProcessors. Here's the list of types that can be processed by default:

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); 
}

7. Parameters

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 ISpecialValueProviders:

  • (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

8. Overloads

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

9. Exceptions

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

10. Helpful methods

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 request null will be returned (by default there's 10 minutes timeout).
  • WaitForAnswerAsync - The same as WaitForUpdateAsync, but it returns only text from Update 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 - Generates ReplyKeyboardMarkup by command methods' names:
MainMenu = ReplyKeyboard
(
    nameof(Lucky),
    nameof(Memento),
    nameof(Sum),
    nameof(Sticker),
    nameof(Help)
);
  • InlineKeyboard - Generates InlineKeyboardMarkup by command methods' names

11. Starting the bot

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();

TODO

[ ] Documentation