add architecture skeleton
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["GymLogAI.TelegramBot/GymLogAI.TelegramBot.csproj", "GymLogAI.TelegramBot/"]
|
||||
COPY ["GymLogAI.Application/GymLogAI.Application.csproj", "GymLogAI.Application/"]
|
||||
COPY ["GymLogAI.AI/GymLogAI.AI.csproj", "GymLogAI.AI/"]
|
||||
COPY ["GymLogAI.Core/GymLogAI.Core.csproj", "GymLogAI.Core/"]
|
||||
COPY ["GymLogAI.Persistence/GymLogAI.Persistence.csproj", "GymLogAI.Persistence/"]
|
||||
RUN dotnet restore "GymLogAI.TelegramBot/GymLogAI.TelegramBot.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/GymLogAI.TelegramBot"
|
||||
RUN dotnet publish "./GymLogAI.TelegramBot.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "GymLogAI.TelegramBot.dll"]
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Telegram.Bot" Version="22.9.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GymLogAI.Application\GymLogAI.Application.csproj" />
|
||||
<ProjectReference Include="..\GymLogAI.AI\GymLogAI.AI.csproj" />
|
||||
<ProjectReference Include="..\GymLogAI.Core\GymLogAI.Core.csproj" />
|
||||
<ProjectReference Include="..\GymLogAI.Persistence\GymLogAI.Persistence.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
using GymLogAI.AI;
|
||||
using GymLogAI.Application.Handlers;
|
||||
using GymLogAI.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Telegram.Bot;
|
||||
|
||||
namespace GymLogAI.TelegramBot;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.Services
|
||||
.AddOptions<TelegramBotOptions>()
|
||||
.Bind(builder.Configuration.GetSection(TelegramBotOptions.SectionName));
|
||||
|
||||
builder.Services.AddPersistence(builder.Configuration);
|
||||
builder.Services.AddAiServices(builder.Configuration);
|
||||
builder.Services.AddScoped<ImportTelegramMessageHandler>();
|
||||
builder.Services.AddScoped<ParseTelegramMessageHandler>();
|
||||
builder.Services.AddScoped<RecommendWorkoutHandler>();
|
||||
builder.Services.AddSingleton<ITelegramBotClient>(serviceProvider =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<Microsoft.Extensions.Options.IOptions<TelegramBotOptions>>().Value;
|
||||
return new TelegramBotClient(options.BotToken ?? string.Empty);
|
||||
});
|
||||
builder.Services.AddScoped<UpdateHandler>();
|
||||
builder.Services.AddHostedService<TelegramBotHostedService>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
await using (var scope = host.Services.CreateAsyncScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await dbContext.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
await host.RunAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace GymLogAI.TelegramBot;
|
||||
|
||||
public sealed class TelegramBotHostedService(
|
||||
ITelegramBotClient botClient,
|
||||
IServiceProvider serviceProvider,
|
||||
IOptions<TelegramBotOptions> options,
|
||||
ILogger<TelegramBotHostedService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Value.BotToken))
|
||||
{
|
||||
logger.LogWarning("Telegram bot token is not configured. Bot polling is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var receiverOptions = new ReceiverOptions
|
||||
{
|
||||
AllowedUpdates = [UpdateType.Message]
|
||||
};
|
||||
|
||||
botClient.StartReceiving(
|
||||
async (client, update, cancellationToken) =>
|
||||
{
|
||||
await using var scope = serviceProvider.CreateAsyncScope();
|
||||
var handler = scope.ServiceProvider.GetRequiredService<UpdateHandler>();
|
||||
await handler.HandleUpdateAsync(client, update, cancellationToken);
|
||||
},
|
||||
async (client, exception, cancellationToken) =>
|
||||
{
|
||||
await using var scope = serviceProvider.CreateAsyncScope();
|
||||
var handler = scope.ServiceProvider.GetRequiredService<UpdateHandler>();
|
||||
await handler.HandleErrorAsync(client, exception, cancellationToken);
|
||||
},
|
||||
receiverOptions,
|
||||
stoppingToken);
|
||||
|
||||
logger.LogInformation("Telegram bot long polling started.");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(options.Value.PollingTimeoutSeconds), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace GymLogAI.TelegramBot;
|
||||
|
||||
public sealed record TelegramBotOptions
|
||||
{
|
||||
public const string SectionName = "TelegramBot";
|
||||
|
||||
public string? BotToken { get; init; }
|
||||
public int PollingTimeoutSeconds { get; init; } = 5;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using GymLogAI.Application.Handlers;
|
||||
using GymLogAI.Core.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
|
||||
namespace GymLogAI.TelegramBot;
|
||||
|
||||
public sealed class UpdateHandler(
|
||||
ImportTelegramMessageHandler importTelegramMessageHandler,
|
||||
RecommendWorkoutHandler recommendWorkoutHandler,
|
||||
ILogger<UpdateHandler> logger)
|
||||
{
|
||||
public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
|
||||
{
|
||||
if (update.Type != UpdateType.Message || update.Message?.Text is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = update.Message;
|
||||
var text = message.Text.Trim();
|
||||
|
||||
if (text.Equals("/start", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await botClient.SendMessage(
|
||||
message.Chat.Id,
|
||||
"GymLogAI bot is ready. Send workout text or use /today for a recommendation.",
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.Equals("/today", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var userId = CreateUserId(message.From?.Id ?? message.Chat.Id);
|
||||
var recommendation = await recommendWorkoutHandler.HandleAsync(
|
||||
new RecommendWorkoutCommand(userId, GoalType.Maintain, "Requested from Telegram command /today"),
|
||||
cancellationToken);
|
||||
|
||||
await botClient.SendMessage(
|
||||
message.Chat.Id,
|
||||
$"{recommendation.Title}\n{recommendation.Summary}\nExercises: {string.Join(", ", recommendation.Exercises)}",
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var importedMessageId = await importTelegramMessageHandler.HandleAsync(
|
||||
new ImportTelegramMessageCommand(
|
||||
CreateUserId(message.From?.Id ?? message.Chat.Id),
|
||||
message.From?.Id ?? message.Chat.Id,
|
||||
message.Chat.Id,
|
||||
message.MessageId,
|
||||
text,
|
||||
message.Date.ToUniversalTime(),
|
||||
message.From?.Username,
|
||||
message.From?.FirstName,
|
||||
message.From?.LastName),
|
||||
cancellationToken);
|
||||
|
||||
logger.LogInformation("Imported telegram message {MessageId} from chat {ChatId}", importedMessageId, message.Chat.Id);
|
||||
|
||||
await botClient.SendMessage(
|
||||
message.Chat.Id,
|
||||
"Message imported. It will be parsed by the worker shortly.",
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogError(exception, "Telegram bot polling error");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Guid CreateUserId(long telegramUserId)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
BitConverter.TryWriteBytes(buffer[..8], telegramUserId);
|
||||
BitConverter.TryWriteBytes(buffer[8..], telegramUserId);
|
||||
return new Guid(buffer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=localhost;Port=5432;Database=gymlogai;Username=postgres;Password=postgres"
|
||||
},
|
||||
"TelegramBot": {
|
||||
"BotToken": "",
|
||||
"PollingTimeoutSeconds": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=localhost;Port=5432;Database=gymlogai;Username=postgres;Password=postgres"
|
||||
},
|
||||
"TelegramBot": {
|
||||
"BotToken": "",
|
||||
"PollingTimeoutSeconds": 5
|
||||
},
|
||||
"OpenRouter": {
|
||||
"BaseUrl": "https://openrouter.ai/api/v1/",
|
||||
"ApiKey": "",
|
||||
"ChatModel": "openai/gpt-4.1-mini",
|
||||
"EmbeddingModel": "text-embedding-3-small"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user