From ecb57d46396fd2a21dcb9f9f2c14b13325d5b760 Mon Sep 17 00:00:00 2001 From: Yehor Hladkov Date: Thu, 5 Mar 2026 01:42:59 +0400 Subject: [PATCH 1/3] =?UTF-8?q?=D0=9D=D0=B0=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RecipeApp/RecipeApp.sln | 8 + RecipeApp/RecipeManagerBot/Bot.http | 6 + .../Controllers/RecipesController.cs | 71 ++++++++ .../Controllers/UploadController.cs | 28 ++++ .../RecipeManagerBot/Data/AppDbContext.cs | 20 +++ RecipeApp/RecipeManagerBot/Dockerfile | 23 +++ RecipeApp/RecipeManagerBot/Models/User.cs | 50 ++++++ RecipeApp/RecipeManagerBot/Program.cs | 50 ++++++ .../Properties/launchSettings.json | 23 +++ .../RecipeManagerBot/RecipeManagerBot.csproj | 23 +++ .../Services/ReceiverService.cs | 30 ++++ .../Services/UpdateHandler.cs | 74 +++++++++ .../appsettings.Development.json | 8 + RecipeApp/RecipeManagerBot/appsettings.json | 5 + Web/RecipeManagerBot/index.html | 157 ++++++++++++++++++ 15 files changed, 576 insertions(+) create mode 100644 RecipeApp/RecipeManagerBot/Bot.http create mode 100644 RecipeApp/RecipeManagerBot/Controllers/RecipesController.cs create mode 100644 RecipeApp/RecipeManagerBot/Controllers/UploadController.cs create mode 100644 RecipeApp/RecipeManagerBot/Data/AppDbContext.cs create mode 100644 RecipeApp/RecipeManagerBot/Dockerfile create mode 100644 RecipeApp/RecipeManagerBot/Models/User.cs create mode 100644 RecipeApp/RecipeManagerBot/Program.cs create mode 100644 RecipeApp/RecipeManagerBot/Properties/launchSettings.json create mode 100644 RecipeApp/RecipeManagerBot/RecipeManagerBot.csproj create mode 100644 RecipeApp/RecipeManagerBot/Services/ReceiverService.cs create mode 100644 RecipeApp/RecipeManagerBot/Services/UpdateHandler.cs create mode 100644 RecipeApp/RecipeManagerBot/appsettings.Development.json create mode 100644 RecipeApp/RecipeManagerBot/appsettings.json create mode 100644 Web/RecipeManagerBot/index.html diff --git a/RecipeApp/RecipeApp.sln b/RecipeApp/RecipeApp.sln index eed883c..a60255e 100644 --- a/RecipeApp/RecipeApp.sln +++ b/RecipeApp/RecipeApp.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RecipeApp", "RecipeApp\RecipeApp.csproj", "{81F7161A-A4D1-456F-ABA2-0FC277F693B1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RecipeManagerBot", "RecipeManagerBot\RecipeManagerBot.csproj", "{1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}" +EndProject EndProject Global @@ -17,5 +19,11 @@ Global {81F7161A-A4D1-456F-ABA2-0FC277F693B1}.Release|Any CPU.Build.0 = Release|Any CPU {81F7161A-A4D1-456F-ABA2-0FC277F693B1}.StyleCheck|Any CPU.ActiveCfg = StyleCheck|Any CPU {81F7161A-A4D1-456F-ABA2-0FC277F693B1}.StyleCheck|Any CPU.Build.0 = StyleCheck|Any CPU + {1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}.Release|Any CPU.Build.0 = Release|Any CPU + {1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}.StyleCheck|Any CPU.ActiveCfg = Debug|Any CPU + {1EC5EA95-4AF2-4D70-8C04-885F0ABF989A}.StyleCheck|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection EndGlobal diff --git a/RecipeApp/RecipeManagerBot/Bot.http b/RecipeApp/RecipeManagerBot/Bot.http new file mode 100644 index 0000000..8d4d6c4 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Bot.http @@ -0,0 +1,6 @@ +@Bot_HostAddress = http://localhost:5129 + +GET {{Bot_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/RecipeApp/RecipeManagerBot/Controllers/RecipesController.cs b/RecipeApp/RecipeManagerBot/Controllers/RecipesController.cs new file mode 100644 index 0000000..93ee958 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Controllers/RecipesController.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RecipeManagerBot.Data; +using RecipeManagerBot.Models; + +namespace RecipeManagerBot.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RecipesController : ControllerBase +{ + private readonly AppDbContext _db; + + public RecipesController(AppDbContext db) + { + _db = db; + } + + // 1. Получить все рецепты пользователя + [HttpGet("user/{telegramId}")] + public async Task GetMyRecipes(long telegramId) + { + var recipes = await _db.Recipes + .Where(r => r.OwnerId == telegramId) + .Include(r => r.Ingredients) + .Include(r => r.Steps) + .ThenInclude(s => s.Images) + .ToListAsync(); + + return Ok(recipes); + } + + // 2. Добавить новый рецепт + [HttpPost] + public async Task CreateRecipe([FromBody] Recipe recipe) + { + if (recipe == null) return BadRequest(); + + _db.Recipes.Add(recipe); + await _db.SaveChangesAsync(); + + return Ok(new { message = "Рецепт успешно сохранен!", recipeId = recipe.Id }); + } + + // 3. Получить конкретный рецепт (для шаринга) + [HttpGet("{id}")] + public async Task GetRecipe(int id) + { + var recipe = await _db.Recipes + .Include(r => r.Ingredients) + .Include(r => r.Steps) + .ThenInclude(s => s.Images) + .FirstOrDefaultAsync(r => r.Id == id); + + if (recipe == null) return NotFound(); + + return Ok(recipe); + } + + // 4. Удалить рецепт + [HttpDelete("{id}")] + public async Task DeleteRecipe(int id) + { + var recipe = await _db.Recipes.FindAsync(id); + if (recipe == null) return NotFound(); + + _db.Recipes.Remove(recipe); + await _db.SaveChangesAsync(); + return Ok(); + } +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/Controllers/UploadController.cs b/RecipeApp/RecipeManagerBot/Controllers/UploadController.cs new file mode 100644 index 0000000..c63c98e --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Controllers/UploadController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; + +[ApiController] +[Route("api/[controller]")] +public class UploadController : ControllerBase +{ + private readonly IWebHostEnvironment _env; + + public UploadController(IWebHostEnvironment env) => _env = env; + + [HttpPost] + public async Task UploadImage(IFormFile file) + { + if (file == null || file.Length == 0) return BadRequest(); + + var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName); + var path = Path.Combine(_env.WebRootPath, "uploads", fileName); + + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { url = $"/uploads/{fileName}" }); + } +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/Data/AppDbContext.cs b/RecipeApp/RecipeManagerBot/Data/AppDbContext.cs new file mode 100644 index 0000000..1ff14f1 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Data/AppDbContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using RecipeManagerBot.Models; + +namespace RecipeManagerBot.Data; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet Users => Set(); + public DbSet Recipes => Set(); + public DbSet Steps => Set(); + public DbSet Ingredients => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Базовая настройка связей, если понадобится кастомизация + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/Dockerfile b/RecipeApp/RecipeManagerBot/Dockerfile new file mode 100644 index 0000000..fdbc80c --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Bot/Bot.csproj", "Bot/"] +RUN dotnet restore "Bot/Bot.csproj" +COPY . . +WORKDIR "/src/Bot" +RUN dotnet build "./Bot.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Bot.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Bot.dll"] diff --git a/RecipeApp/RecipeManagerBot/Models/User.cs b/RecipeApp/RecipeManagerBot/Models/User.cs new file mode 100644 index 0000000..f9a5b7e --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Models/User.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace RecipeManagerBot.Models; + +// Пользователь бота +public class User +{ + [Key] + public long TelegramId { get; set; } + public string Username { get; set; } = string.Empty; + public List MyRecipes { get; set; } = new(); +} + +// Сам рецепт +public class Recipe +{ + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public List Ingredients { get; set; } = new(); + public List Steps { get; set; } = new(); + + public long OwnerId { get; set; } + public bool IsPublic { get; set; } = false; // Для шаринга другим +} + +// Ингредиент (связан с рецептом) +public class Ingredient +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Amount { get; set; } = string.Empty; // Напр: "100г" или "2 шт" +} + +// Шаг приготовления +public class RecipeStep +{ + public int Id { get; set; } + public int StepNumber { get; set; } + public string Instruction { get; set; } = string.Empty; + + // Список URL или FileId картинок для этого шага + public List Images { get; set; } = new(); +} + +public class StepImage +{ + public int Id { get; set; } + public string Url { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/Program.cs b/RecipeApp/RecipeManagerBot/Program.cs new file mode 100644 index 0000000..dce5c76 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Program.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using RecipeManagerBot.Data; +using RecipeManagerBot.Services; +using Telegram.Bot; + +namespace RecipeManagerBot; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // 1. БД (SQLite) + builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=recipes.db")); + + // 2. Регистрация Клиента Телеграм + builder.Services.AddSingleton(sp => + new TelegramBotClient(builder.Configuration["BotConfiguration:BotToken"]!)); + + // 3. Наши сервисы + builder.Services.AddSingleton(); + builder.Services.AddHostedService(); + + builder.Services.AddCors(options => + { + options.AddPolicy("AllowVercel", policy => + { + policy.AllowAnyOrigin() // Позже заменишь на конкретный URL от Vercel + .AllowAnyHeader() + .AllowAnyMethod(); + }); + }); + + var app = builder.Build(); + + app.UseCors("AllowVercel"); + // Авто-создание БД при запуске (удобно для разработки) + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + } + + app.UseStaticFiles(); // Чтобы картинки были доступны по ссылке + app.MapControllers(); + app.Run(); + } +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/Properties/launchSettings.json b/RecipeApp/RecipeManagerBot/Properties/launchSettings.json new file mode 100644 index 0000000..40f4645 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7170;http://localhost:5129", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RecipeApp/RecipeManagerBot/RecipeManagerBot.csproj b/RecipeApp/RecipeManagerBot/RecipeManagerBot.csproj new file mode 100644 index 0000000..b615326 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/RecipeManagerBot.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + Linux + + + + + + + + + + + + .dockerignore + + + + diff --git a/RecipeApp/RecipeManagerBot/Services/ReceiverService.cs b/RecipeApp/RecipeManagerBot/Services/ReceiverService.cs new file mode 100644 index 0000000..e666efb --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Services/ReceiverService.cs @@ -0,0 +1,30 @@ +using Telegram.Bot; +using Telegram.Bot.Polling; + +namespace RecipeManagerBot.Services; + +public class ReceiverService : BackgroundService +{ + private readonly ITelegramBotClient _botClient; + private readonly UpdateHandler _updateHandler; + + public ReceiverService(ITelegramBotClient botClient, UpdateHandler updateHandler) + { + _botClient = botClient; + _updateHandler = updateHandler; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var options = new ReceiverOptions { AllowedUpdates = [] }; // Слушать всё + + _botClient.StartReceiving( + updateHandler: (bot, update, ct) => _updateHandler.HandleUpdateAsync(update, ct), + errorHandler: (bot, ex, ct) => _updateHandler.HandlePollingErrorAsync(bot, ex, ct), + receiverOptions: options, + cancellationToken: stoppingToken + ); + + await Task.Delay(Timeout.Infinite, stoppingToken); + } +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/Services/UpdateHandler.cs b/RecipeApp/RecipeManagerBot/Services/UpdateHandler.cs new file mode 100644 index 0000000..79fb2db --- /dev/null +++ b/RecipeApp/RecipeManagerBot/Services/UpdateHandler.cs @@ -0,0 +1,74 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.ReplyMarkups; +using RecipeManagerBot.Data; +using RecipeManagerBot.Models; +using Microsoft.EntityFrameworkCore; + +namespace RecipeManagerBot.Services; + +public class UpdateHandler +{ + private readonly ITelegramBotClient _botClient; + private readonly IServiceScopeFactory _scopeFactory; + + public UpdateHandler(ITelegramBotClient botClient, IServiceScopeFactory scopeFactory) + { + _botClient = botClient; + _scopeFactory = scopeFactory; + } + + public async Task HandleUpdateAsync(Update update, CancellationToken ct) + { + // Нас интересуют только текстовые сообщения + if (update.Message is not { Text: { } messageText } message) return; + + var chatId = message.Chat.Id; + + if (messageText == "/start") + { + await HandleStartCommand(message, ct); + } + } + + private async Task HandleStartCommand(Message message, CancellationToken ct) + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // 1. Регистрируем или обновляем пользователя в БД + var user = await db.Users.FirstOrDefaultAsync(u => u.TelegramId == message.From!.Id, ct); + if (user == null) + { + user = new Models.User + { + TelegramId = message.From!.Id, + Username = message.From.Username ?? "Anonymous" + }; + db.Users.Add(user); + await db.SaveChangesAsync(ct); + } + + // 2. Отправляем кнопку Mini App + // ВАЖНО: Замени URL на свой, когда задеплоишь фронтенд (пока можно оставить заглушку) + var webAppUrl = "https://your-mini-app-url.com"; + + var keyboard = new InlineKeyboardMarkup( + InlineKeyboardButton.WithWebApp("Открыть книгу рецептов 📖", new WebAppInfo { Url = webAppUrl }) + ); + + await _botClient.SendMessage( + chatId: message.Chat.Id, + text: $"Привет, {user.Username}! Готов создавать шедевры? Нажми кнопку ниже, чтобы управлять своими рецептами.", + replyMarkup: keyboard, + cancellationToken: ct + ); + } + + public Task HandlePollingErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken ct) + { + Console.WriteLine("Ошибка API: " + exception.Message); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/RecipeApp/RecipeManagerBot/appsettings.Development.json b/RecipeApp/RecipeManagerBot/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RecipeApp/RecipeManagerBot/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RecipeApp/RecipeManagerBot/appsettings.json b/RecipeApp/RecipeManagerBot/appsettings.json new file mode 100644 index 0000000..2fc6a80 --- /dev/null +++ b/RecipeApp/RecipeManagerBot/appsettings.json @@ -0,0 +1,5 @@ +{ + "BotConfiguration": { + "BotToken": "8313558915:AAH903QP8lV6lhOzHH0ryKqOr5O9ybfJCrU" + } +} \ No newline at end of file diff --git a/Web/RecipeManagerBot/index.html b/Web/RecipeManagerBot/index.html new file mode 100644 index 0000000..7a4a36a --- /dev/null +++ b/Web/RecipeManagerBot/index.html @@ -0,0 +1,157 @@ + + + + + + Кулинарная Книга + + + + + +
+
+

Новый рецепт

+ +
+ +
+ + +
+ +
+

Ингредиенты

+
+ +
+ +
+

Шаги приготовления

+
+ +
+ +
+ +
+
+ + + + \ No newline at end of file From b0da97988599b6ab087b906f2d94f2a9bd36bd95 Mon Sep 17 00:00:00 2001 From: Yehor Hladkov Date: Thu, 5 Mar 2026 02:15:01 +0400 Subject: [PATCH 2/3] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 177d88f..395a86f 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,5 @@ RecipeApp/RecipeApp/wwwroot/uploads/ RecipeApp/RecipeApp/appsettings.json RecipeApp/RecipeApp/appsettings.Development.json + +Web/RecipeManagerBot/.env From f6637638e6b91d926d20eadb462c8578f7f9ffe7 Mon Sep 17 00:00:00 2001 From: Yehor Hladkov Date: Thu, 5 Mar 2026 02:15:10 +0400 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=84=D1=80=D0=BE=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RecipeApp/RecipeManagerBot/appsettings.json | 2 +- Web/RecipeManagerBot/index.html | 6 ++++-- Web/RecipeManagerBot/package.json | 16 ++++++++++++++++ Web/RecipeManagerBot/vercel.json | 20 ++++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 Web/RecipeManagerBot/package.json create mode 100644 Web/RecipeManagerBot/vercel.json diff --git a/RecipeApp/RecipeManagerBot/appsettings.json b/RecipeApp/RecipeManagerBot/appsettings.json index 2fc6a80..894834e 100644 --- a/RecipeApp/RecipeManagerBot/appsettings.json +++ b/RecipeApp/RecipeManagerBot/appsettings.json @@ -1,5 +1,5 @@ { "BotConfiguration": { - "BotToken": "8313558915:AAH903QP8lV6lhOzHH0ryKqOr5O9ybfJCrU" + "BotToken": "8313558915:AAEY0cJPX69eddXQKFZX2FuV01q4jDcHPpc" } } \ No newline at end of file diff --git a/Web/RecipeManagerBot/index.html b/Web/RecipeManagerBot/index.html index 7a4a36a..2023909 100644 --- a/Web/RecipeManagerBot/index.html +++ b/Web/RecipeManagerBot/index.html @@ -50,8 +50,10 @@