Skip to content

Commit

Permalink
Minor refactoring here and there.
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-kondratov committed Aug 20, 2024
1 parent 238a0c7 commit 7c049fc
Show file tree
Hide file tree
Showing 15 changed files with 252 additions and 262 deletions.
102 changes: 49 additions & 53 deletions src/PillsBot/PillsBot.Server/BotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,74 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PillsBot.Server.Chat;
using PillsBot.Server.Configuration;
using PillsBot.Server.TextGeneration;

namespace PillsBot.Server
namespace PillsBot.Server;

internal class BotService(ILogger<BotService> logger, IChatClient chatClient,
IOptions<PillsBotOptions> options, IServiceProvider serviceProvider) : BackgroundService
{
internal class BotService(ILogger<BotService> logger, IChatClient chatClient,
IOptions<PillsBotOptions> options, IServiceProvider serviceProvider)
: BackgroundService
private readonly ILogger<BotService> _logger = logger;
private readonly IChatClient _chatClient = chatClient;
private readonly PillsBotOptions _options = options.Value;
private readonly IServiceProvider _serviceProvider = serviceProvider;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
private readonly ILogger<BotService> _logger = logger;
private readonly IChatClient _chatClient = chatClient;
private readonly PillsBotOptions _options = options.Value;
private readonly IServiceProvider _serviceProvider = serviceProvider;
_logger.LogInformation("Starting bot version {Version}.", typeof(BotService).GetAssemblyVersionString());

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
try
{
_logger.LogInformation("Starting bot...");

try
{
await _chatClient.Start(stoppingToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to start the chat client.");
return;
}
await _chatClient.Start(stoppingToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to start the chat client.");
return;
}

_logger.LogInformation("Bot started.");
_logger.LogInformation("Bot started.");

DateTime begins = _options.Reminder.Begins;
TimeSpan interval = _options.Reminder.Interval;
DateTime begins = _options.Reminder.Begins;
TimeSpan interval = _options.Reminder.Interval;

DateTime next = GetNext(begins, interval);
_logger.LogInformation("Next reminder comes off at {Next}", next);
DateTime next = GetNext(begins, interval);
_logger.LogInformation("Next reminder comes off at {Next}", next);

while (!stoppingToken.IsCancellationRequested)
while (!stoppingToken.IsCancellationRequested)
{
if (next <= DateTime.Now)
{
if (next <= DateTime.Now)
{
await using AsyncServiceScope scope = _serviceProvider.CreateAsyncScope();

IMessageProvider messageProvider = scope.ServiceProvider.GetRequiredService<IMessageProvider>();
(string reminder, string button, string appreciation) = await messageProvider.GetMessage(stoppingToken);
await _chatClient.Notify(reminder, button, appreciation, stoppingToken);
await using AsyncServiceScope scope = _serviceProvider.CreateAsyncScope();

next = GetNext(begins, interval);
_logger.LogInformation("Next reminder comes off at {Next}", next);
}
IMessageProvider messageProvider = scope.ServiceProvider.GetRequiredService<IMessageProvider>();
(string reminder, string button, string appreciation) = await messageProvider.GetMessage(stoppingToken);
await _chatClient.Notify(reminder, button, appreciation, stoppingToken);

await Task.Delay(1000, stoppingToken);
next = GetNext(begins, interval);
_logger.LogInformation("Next reminder comes off at {Next}", next);
}

_logger.LogInformation("Bot stopped.");
await Task.Delay(1000, stoppingToken);
}

private static DateTime GetNext(DateTime begins, TimeSpan interval)
{
DateTime now = DateTime.Now;
if (now < begins)
{
return begins;
}
_logger.LogInformation("Bot stopped.");
}

DateTime current = begins;
while (current < now)
{
current = current.Add(interval);
}
private static DateTime GetNext(DateTime begins, TimeSpan interval)
{
DateTime now = DateTime.Now;
if (now < begins)
{
return begins;
}

return current;
DateTime current = begins;
while (current < now)
{
current = current.Add(interval);
}

return current;
}
}
11 changes: 0 additions & 11 deletions src/PillsBot/PillsBot.Server/Chat/IChatClient.cs

This file was deleted.

10 changes: 10 additions & 0 deletions src/PillsBot/PillsBot.Server/Chat/Interfaces/IChatClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;

namespace PillsBot.Server;

internal interface IChatClient
{
Task Start(CancellationToken cancellationToken = default);
Task Notify(string reminder, string button, string appreciation, CancellationToken cancellationToken = default);
}
170 changes: 84 additions & 86 deletions src/PillsBot/PillsBot.Server/Chat/TelegramChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,120 +4,118 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PillsBot.Server.Configuration;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;

namespace PillsBot.Server.Chat
namespace PillsBot.Server;

internal class TelegramChatClient(ILogger<TelegramChatClient> logger, IOptions<PillsBotOptions> options)
: IChatClient, IUpdateHandler
{
internal class TelegramChatClient(ILogger<TelegramChatClient> logger, IOptions<PillsBotOptions> options)
: IChatClient, IUpdateHandler
private static readonly ReceiverOptions ReceiverOptions = new()
{
private static readonly ReceiverOptions ReceiverOptions = new()
{
AllowedUpdates = [UpdateType.Message, UpdateType.CallbackQuery]
};
AllowedUpdates = [UpdateType.Message, UpdateType.CallbackQuery]
};

private readonly ILogger<TelegramChatClient> _logger = logger;
private readonly PillsBotOptions _options = options.Value;
private readonly TelegramBotClient _client = new(options.Value.Telegram?.ApiToken ?? throw new InvalidOperationException("Missing Telegram API token."));
private readonly ILogger<TelegramChatClient> _logger = logger;
private readonly PillsBotOptions _options = options.Value;
private readonly TelegramBotClient _client = new(options.Value.Telegram?.ApiToken ?? throw new InvalidOperationException("Missing Telegram API token."));

public async Task Start(CancellationToken cancellationToken = default)
public async Task Start(CancellationToken cancellationToken = default)
{
bool valid = await _client.TestApiAsync(cancellationToken);
if (!valid)
{
bool valid = await _client.TestApiAsync(cancellationToken);
if (!valid)
{
throw new InvalidOperationException("Telegram token validation failed.");
}

// webhooks not supported
WebhookInfo webhookInfo = await _client.GetWebhookInfoAsync(cancellationToken);
if (!string.IsNullOrEmpty(webhookInfo.Url))
{
_logger.LogWarning("A webhook is set up on the server. Deleting...");
await _client.DeleteWebhookAsync(true, cancellationToken);
}

_client.StartReceiving(this, ReceiverOptions, cancellationToken);
_logger.LogInformation("Started receiving updates.");
throw new InvalidOperationException("Telegram token validation failed.");
}

public async Task Notify(string reminder, string button, string appreciation, CancellationToken cancellationToken = default)
// webhooks not supported
WebhookInfo webhookInfo = await _client.GetWebhookInfoAsync(cancellationToken);
if (!string.IsNullOrEmpty(webhookInfo.Url))
{
ChatId chatId = _options.Telegram?.ChatId ??
throw new InvalidOperationException("Chat id not configured");
_logger.LogWarning("A webhook is set up on the server. Deleting...");
await _client.DeleteWebhookAsync(true, cancellationToken);
}

IReplyMarkup replyMarkup = GetReplyMarkup(button, appreciation);
_client.StartReceiving(this, ReceiverOptions, cancellationToken);
_logger.LogInformation("Started receiving updates.");
}

_logger.LogInformation("Sending message: {Message} to chat {ChatId}", reminder, chatId);
await _client.SendTextMessageAsync(chatId, reminder, replyMarkup: replyMarkup,
cancellationToken: cancellationToken);
public async Task Notify(string reminder, string button, string appreciation, CancellationToken cancellationToken = default)
{
ChatId chatId = _options.Telegram?.ChatId ??
throw new InvalidOperationException("Chat id not configured");

_logger.LogInformation("Message sent.");
}
IReplyMarkup replyMarkup = GetReplyMarkup(button, appreciation);

public Task HandleUpdateAsync(ITelegramBotClient botClient, Update update,
CancellationToken cancellationToken)
{
return update.Type switch
{
UpdateType.CallbackQuery => OnCallbackQuery(update.CallbackQuery, cancellationToken),
UpdateType.Message => OnClientMessage(update.Message),
_ => throw new InvalidEnumArgumentException("UpdateType", (int)update.Type, typeof(UpdateType))
};
}
_logger.LogInformation("Sending message: {Message} to chat {ChatId}", reminder, chatId);
await _client.SendTextMessageAsync(chatId, reminder, replyMarkup: replyMarkup,
cancellationToken: cancellationToken);

_logger.LogInformation("Message sent.");
}

public Task HandlePollingErrorAsync(ITelegramBotClient botClient, Exception exception,
CancellationToken cancellationToken)
public Task HandleUpdateAsync(ITelegramBotClient botClient, Update update,
CancellationToken cancellationToken)
{
return update.Type switch
{
_logger.LogError(exception, "Polling error.");
return Task.CompletedTask;
}
UpdateType.CallbackQuery => OnCallbackQuery(update.CallbackQuery, cancellationToken),
UpdateType.Message => OnClientMessage(update.Message),
_ => throw new InvalidEnumArgumentException("UpdateType", (int)update.Type, typeof(UpdateType))
};
}

public Task HandlePollingErrorAsync(ITelegramBotClient botClient, Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "Polling error.");
return Task.CompletedTask;
}

private async Task OnCallbackQuery(CallbackQuery? query, CancellationToken cancellationToken)
private async Task OnCallbackQuery(CallbackQuery? query, CancellationToken cancellationToken)
{
_logger.LogTrace("A callback query received: {@CallbackQuery}", query);

if (query?.Message is null)
{
_logger.LogTrace("A callback query received: {@CallbackQuery}", query);

if (query?.Message is null)
{
_logger.LogWarning("Callback query message was empty. Enable trace log level to see the details.");
return;
}

long chatId = query.Message.Chat.Id;
if (chatId != _options.Telegram!.ChatId)
{
_logger.LogWarning("Unexpected chat id in callback query: {@CallbackQuery}", query);
return;
}

// fire message removal
await _client.DeleteMessageAsync(chatId, query.Message.MessageId, cancellationToken);

// fire callback
await _client.AnswerCallbackQueryAsync(query.Id, query.Data, cancellationToken: cancellationToken);
_logger.LogWarning("Callback query message was empty. Enable trace log level to see the details.");
return;
}

private Task OnClientMessage(Message? message)
long chatId = query.Message.Chat.Id;
if (chatId != _options.Telegram!.ChatId)
{
_logger.LogInformation("A message received: {@Message}", message);
return Task.CompletedTask;
_logger.LogWarning("Unexpected chat id in callback query: {@CallbackQuery}", query);
return;
}

private InlineKeyboardMarkup GetReplyMarkup(string button, string appreciation)
// fire message removal
await _client.DeleteMessageAsync(chatId, query.Message.MessageId, cancellationToken);

// fire callback
await _client.AnswerCallbackQueryAsync(query.Id, query.Data, cancellationToken: cancellationToken);
}

private Task OnClientMessage(Message? message)
{
_logger.LogInformation("A message received: {@Message}", message);
return Task.CompletedTask;
}

private InlineKeyboardMarkup GetReplyMarkup(string button, string appreciation)
{
if (appreciation.Length > 64)
{
if (appreciation.Length > 64)
{
_logger.LogWarning("Appreciation message \"{Appreciation}\" length is greater than 64. Will truncate.", appreciation);
appreciation = appreciation[..64];
}

var inlineKeyboardButton = InlineKeyboardButton.WithCallbackData(button, appreciation);
var result = new InlineKeyboardMarkup(inlineKeyboardButton);
return result;
_logger.LogWarning("Appreciation message \"{Appreciation}\" length is greater than 64. Will truncate.", appreciation);
appreciation = appreciation[..64];
}

var inlineKeyboardButton = InlineKeyboardButton.WithCallbackData(button, appreciation);
var result = new InlineKeyboardMarkup(inlineKeyboardButton);
return result;
}
}
33 changes: 16 additions & 17 deletions src/PillsBot/PillsBot.Server/Configuration/AIOptions.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
using Microsoft.Extensions.Logging;

namespace PillsBot.Server.Configuration
namespace PillsBot.Server;

public class AIOptions
{
public class AIOptions
{
public bool Enabled { get; set; } = false;
public string Languages { get; set; } = "en-US";
public string PetNames { get; set; } = "unknown";
public string PetGender { get; set; } = "unknown";
public LogLevel LogLevel { get; set; } = LogLevel.Warning;
public AzureOpenAIOptions? Azure { get; set; } = null;
public int ChoicesCount { get; set; } = 20;
public int MaxTokens { get; set; } = 1000;
public bool Enabled { get; set; } = false;
public string Languages { get; set; } = "en-US";
public string PetNames { get; set; } = "unknown";
public string PetGender { get; set; } = "unknown";
public LogLevel LogLevel { get; set; } = LogLevel.Warning;
public AzureOpenAIOptions? Azure { get; set; } = null;
public int ChoicesCount { get; set; } = 20;
public int MaxTokens { get; set; } = 1000;

public class AzureOpenAIOptions
{
public required string Endpoint { get; set; }
public required string Key { get; set; }
public required string DeploymentName { get; set; }
}
public class AzureOpenAIOptions
{
public required string Endpoint { get; set; }
public required string Key { get; set; }
public required string DeploymentName { get; set; }
}
}
Loading

0 comments on commit 7c049fc

Please sign in to comment.