Skip to content

Commit

Permalink
Fix Webhook.Controllers (STJ) & harmonize with Console.Advanced
Browse files Browse the repository at this point in the history
  • Loading branch information
wiz0u committed Jul 2, 2024
1 parent 5de3158 commit c261ac4
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 288 deletions.
File renamed without changes.
21 changes: 10 additions & 11 deletions Console.Advanced/Services/UpdateHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ public UpdateHandler(ITelegramBotClient bot, ILogger<UpdateHandler> logger)
_logger = logger;
}

public async Task HandleErrorAsync(ITelegramBotClient bot, Exception exception, HandleErrorSource source, CancellationToken cancellationToken)
{
_logger.LogInformation("HandleError: {exception}", exception);
// Cooldown in case of network connection error
if (exception is RequestException)
await Task.Delay(TimeSpan.FromSeconds(2));
}

public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Expand All @@ -35,7 +43,7 @@ public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update,
// UpdateType.EditedChannelPost:
// UpdateType.ShippingQuery:
// UpdateType.PreCheckoutQuery:
_ => UnknownUpdateHandlerAsync(update)
_ => UnknownUpdateHandlerAsync(update)
});
}

Expand Down Expand Up @@ -152,7 +160,7 @@ private async Task OnCallbackQuery(CallbackQuery callbackQuery)
{
_logger.LogInformation("Received inline keyboard callback from: {CallbackQueryId}", callbackQuery.Id);
await _bot.AnswerCallbackQueryAsync(callbackQuery.Id, $"Received {callbackQuery.Data}");
await _bot.SendTextMessageAsync(callbackQuery.Message!.Chat.Id, $"Received {callbackQuery.Data}");
await _bot.SendTextMessageAsync(callbackQuery.Message!.Chat, $"Received {callbackQuery.Data}");
}

#region Inline Mode
Expand Down Expand Up @@ -194,13 +202,4 @@ private Task UnknownUpdateHandlerAsync(Update update)
_logger.LogInformation("Unknown update type: {UpdateType}", update.Type);
return Task.CompletedTask;
}

public async Task HandleErrorAsync(ITelegramBotClient bot, Exception exception, HandleErrorSource source, CancellationToken cancellationToken)
{
_logger.LogInformation("HandleError: {exception}", exception);

// Cooldown in case of network connection error
if (exception is RequestException)
await Task.Delay(TimeSpan.FromSeconds(2));
}
}
11 changes: 9 additions & 2 deletions Webhook.Controllers/Controllers/BotController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ public class BotController : ControllerBase
[ValidateTelegramBot]
public async Task<IActionResult> Post(
[FromBody] Update update,
[FromServices] UpdateHandlers handleUpdateService,
[FromServices] UpdateHandler handleUpdateService,
CancellationToken cancellationToken)
{
await handleUpdateService.HandleUpdateAsync(update, cancellationToken);
try
{
await handleUpdateService.HandleUpdateAsync(update, cancellationToken);
}
catch (Exception exception)

Check warning on line 21 in Webhook.Controllers/Controllers/BotController.cs

View workflow job for this annotation

GitHub Actions / build

Modify 'Post' to catch a more specific allowed exception type, or rethrow the exception (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1031)
{
await handleUpdateService.HandleErrorAsync(exception, Polling.HandleErrorSource.HandleUpdateError, cancellationToken);
}
return Ok();
}
}
14 changes: 6 additions & 8 deletions Webhook.Controllers/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,18 @@
});

// Dummy business-logic service
builder.Services.AddScoped<UpdateHandlers>();
builder.Services.AddScoped<UpdateHandler>();

// There are several strategies for completing asynchronous tasks during startup.
// Some of them could be found in this article https://andrewlock.net/running-async-tasks-on-app-startup-in-asp-net-core-part-1/
// We are going to use IHostedService to add and later remove Webhook
builder.Services.AddHostedService<ConfigureWebhook>();

// The Telegram.Bot library heavily depends on Newtonsoft.Json library to deserialize
// incoming webhook updates and send serialized responses back.
// Read more about adding Newtonsoft.Json to ASP.NET Core pipeline:
// https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-6.0#add-newtonsoftjson-based-json-format-support
builder.Services
.AddControllers()
.AddNewtonsoftJson();
builder.Services.AddControllers();

// The Telegram.Bot library heavily depends on System.Text.Json library with special Json
// settings to deserialize incoming webhook updates and send serialized responses back.
builder.Services.ConfigureTelegramBotMvc();

var app = builder.Build();
// Construct webhook route from the Route configuration parameter
Expand Down
205 changes: 205 additions & 0 deletions Webhook.Controllers/Services/UpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using Telegram.Bot.Exceptions;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.InlineQueryResults;
using Telegram.Bot.Types.ReplyMarkups;

namespace Telegram.Bot.Services;

public class UpdateHandler
{
private readonly ITelegramBotClient _bot;
private readonly ILogger<UpdateHandler> _logger;
private static readonly InputPollOption[] PollOptions = ["Hello", "World!"];

public UpdateHandler(ITelegramBotClient bot, ILogger<UpdateHandler> logger)
{
_bot = bot;
_logger = logger;
}

public async Task HandleErrorAsync(Exception exception, HandleErrorSource source, CancellationToken cancellationToken)
{
_logger.LogInformation("HandleError: {exception}", exception);

Check warning on line 24 in Webhook.Controllers/Services/UpdateHandler.cs

View workflow job for this annotation

GitHub Actions / build

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 24 in Webhook.Controllers/Services/UpdateHandler.cs

View workflow job for this annotation

GitHub Actions / build

Use PascalCase for named placeholders in the logging message template (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1727)
// Cooldown in case of network connection error
if (exception is RequestException)
await Task.Delay(TimeSpan.FromSeconds(2));

Check warning on line 27 in Webhook.Controllers/Services/UpdateHandler.cs

View workflow job for this annotation

GitHub Actions / build

Forward the 'cancellationToken' parameter to the 'Delay' method or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2016)
}

public async Task HandleUpdateAsync(Update update, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await (update switch
{
{ Message: { } message } => OnMessage(message),
{ EditedMessage: { } message } => OnMessage(message),
{ CallbackQuery: { } callbackQuery } => OnCallbackQuery(callbackQuery),
{ InlineQuery: { } inlineQuery } => OnInlineQuery(inlineQuery),
{ ChosenInlineResult: { } chosenInlineResult } => OnChosenInlineResult(chosenInlineResult),
{ Poll: { } poll } => OnPoll(poll),
{ PollAnswer: { } pollAnswer } => OnPollAnswer(pollAnswer),
// UpdateType.ChannelPost:
// UpdateType.EditedChannelPost:
// UpdateType.ShippingQuery:
// UpdateType.PreCheckoutQuery:
_ => UnknownUpdateHandlerAsync(update)
});
}

private async Task OnMessage(Message msg)
{
_logger.LogInformation("Receive message type: {MessageType}", msg.Type);

Check warning on line 52 in Webhook.Controllers/Services/UpdateHandler.cs

View workflow job for this annotation

GitHub Actions / build

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)
if (msg.Text is not { } messageText)
return;

Message sentMessage = await (messageText.Split(' ')[0] switch
{
"/photo" => SendPhoto(msg),
"/inline_buttons" => SendInlineKeyboard(msg),
"/keyboard" => SendReplyKeyboard(msg),
"/remove" => RemoveKeyboard(msg),
"/request" => RequestContactAndLocation(msg),
"/inline_mode" => StartInlineQuery(msg),
"/poll" => SendPoll(msg),
"/poll_anonymous" => SendAnonymousPoll(msg),
"/throw" => FailingHandler(msg),
_ => Usage(msg)
});
_logger.LogInformation("The message was sent with id: {SentMessageId}", sentMessage.MessageId);

Check warning on line 69 in Webhook.Controllers/Services/UpdateHandler.cs

View workflow job for this annotation

GitHub Actions / build

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)
}

async Task<Message> Usage(Message msg)
{
const string usage = """
<b><u>Bot menu</u></b>:
/photo - send a photo
/inline_buttons - send inline buttons
/keyboard - send keyboard buttons
/remove - remove keyboard buttons
/request - request location or contact
/inline_mode - send inline-mode results list
/poll - send a poll
/poll_anonymous - send an anonymous poll
/throw - what happens if handler fails
""";
return await _bot.SendTextMessageAsync(msg.Chat, usage, parseMode: ParseMode.Html, replyMarkup: new ReplyKeyboardRemove());
}

async Task<Message> SendPhoto(Message msg)
{
await _bot.SendChatActionAsync(msg.Chat, ChatAction.UploadPhoto);
await Task.Delay(2000); // simulate a long task
await using var fileStream = new FileStream("Files/tux.png", FileMode.Open, FileAccess.Read);
return await _bot.SendPhotoAsync(msg.Chat, fileStream, caption: "Read https://telegrambots.github.io/book/");
}

// Send inline keyboard. You can process responses in OnCallbackQuery handler
async Task<Message> SendInlineKeyboard(Message msg)
{
List<List<InlineKeyboardButton>> buttons =
[
["1.1", "1.2", "1.3"],
[
InlineKeyboardButton.WithCallbackData("WithCallbackData", "CallbackData"),
InlineKeyboardButton.WithUrl("WithUrl", "https://github.com/TelegramBots/Telegram.Bot")
],
];
return await _bot.SendTextMessageAsync(msg.Chat, "Inline buttons:", replyMarkup: new InlineKeyboardMarkup(buttons));
}

async Task<Message> SendReplyKeyboard(Message msg)
{
List<List<KeyboardButton>> keys =
[
["1.1", "1.2", "1.3"],
["2.1", "2.2"],
];
return await _bot.SendTextMessageAsync(msg.Chat, "Keyboard buttons:", replyMarkup: new ReplyKeyboardMarkup(keys) { ResizeKeyboard = true });
}

async Task<Message> RemoveKeyboard(Message msg)
{
return await _bot.SendTextMessageAsync(msg.Chat, "Removing keyboard", replyMarkup: new ReplyKeyboardRemove());
}

async Task<Message> RequestContactAndLocation(Message msg)
{
List<KeyboardButton> buttons =
[
KeyboardButton.WithRequestLocation("Location"),
KeyboardButton.WithRequestContact("Contact"),
];
return await _bot.SendTextMessageAsync(msg.Chat, "Who or Where are you?", replyMarkup: new ReplyKeyboardMarkup(buttons));
}

async Task<Message> StartInlineQuery(Message msg)
{
var button = InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("Inline Mode");
return await _bot.SendTextMessageAsync(msg.Chat, "Press the button to start Inline Query\n\n" +
"(Make sure you enabled Inline Mode in @BotFather)", replyMarkup: new InlineKeyboardMarkup(button));
}

async Task<Message> SendPoll(Message msg)
{
return await _bot.SendPollAsync(msg.Chat, "Question", PollOptions, isAnonymous: false);
}

async Task<Message> SendAnonymousPoll(Message msg)
{
return await _bot.SendPollAsync(chatId: msg.Chat, "Question", PollOptions);
}

static Task<Message> FailingHandler(Message msg)
{
throw new IndexOutOfRangeException();
}

// Process Inline Keyboard callback data
private async Task OnCallbackQuery(CallbackQuery callbackQuery)
{
_logger.LogInformation("Received inline keyboard callback from: {CallbackQueryId}", callbackQuery.Id);
await _bot.AnswerCallbackQueryAsync(callbackQuery.Id, $"Received {callbackQuery.Data}");
await _bot.SendTextMessageAsync(callbackQuery.Message!.Chat, $"Received {callbackQuery.Data}");
}

#region Inline Mode

private async Task OnInlineQuery(InlineQuery inlineQuery)
{
_logger.LogInformation("Received inline query from: {InlineQueryFromId}", inlineQuery.From.Id);

InlineQueryResult[] results = [ // displayed result
new InlineQueryResultArticle("1", "Telegram.Bot", new InputTextMessageContent("hello")),
new InlineQueryResultArticle("2", "is the best", new InputTextMessageContent("world"))
];
await _bot.AnswerInlineQueryAsync(inlineQuery.Id, results, cacheTime: 0, isPersonal: true);
}

private async Task OnChosenInlineResult(ChosenInlineResult chosenInlineResult)
{
_logger.LogInformation("Received inline result: {ChosenInlineResultId}", chosenInlineResult.ResultId);
await _bot.SendTextMessageAsync(chosenInlineResult.From.Id, $"You chose result with Id: {chosenInlineResult.ResultId}");
}

#endregion

private Task OnPoll(Poll poll)
{
_logger.LogInformation($"Received Pull info: {poll.Question}");
return Task.CompletedTask;
}

private async Task OnPollAnswer(PollAnswer pollAnswer)
{
var answer = pollAnswer.OptionIds.FirstOrDefault();
var selectedOption = PollOptions[answer];
await _bot.SendTextMessageAsync(pollAnswer.User.Id, $"You've chosen: {selectedOption.Text} in poll");

Check warning on line 197 in Webhook.Controllers/Services/UpdateHandler.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
}

private Task UnknownUpdateHandlerAsync(Update update)
{
_logger.LogInformation("Unknown update type: {UpdateType}", update.Type);
return Task.CompletedTask;
}
}
Loading

0 comments on commit c261ac4

Please sign in to comment.