From 99df15fb31d2a1b4c0ef0d09aa3e429ea5bff350 Mon Sep 17 00:00:00 2001 From: Dmitrii Prokudin Date: Thu, 26 Dec 2024 03:32:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=84=D0=B8=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy.yml | 2 +- Data/UserOfTheDayRepository.cs | 72 +++++++++++++ Data/UserRepository.cs | 41 ++++++++ Model/UserOfTheDayType.cs | 7 ++ Program.cs | 100 +++++------------- Services/BotService.cs | 187 +++++++++++++++++++++++++++++++++ appsettings.json | 8 ++ 7 files changed, 340 insertions(+), 77 deletions(-) create mode 100644 Data/UserOfTheDayRepository.cs create mode 100644 Data/UserRepository.cs create mode 100644 Model/UserOfTheDayType.cs create mode 100644 Services/BotService.cs create mode 100644 appsettings.json diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 4f0451b..42b706c 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -35,5 +35,5 @@ jobs: -e DATABASE_NAME=telegram_bot \ -e DATABASE_USER=postgres \ -e DATABASE_PASSWORD=${{ secrets.DB_PASSWORD }} \ - -e BOT_TOKEN=${{ secrets.BOT_TOKEN }} \ + -e BotSettings_BotToken=${{ secrets.BOT_TOKEN }} \ telegram-bot:latest diff --git a/Data/UserOfTheDayRepository.cs b/Data/UserOfTheDayRepository.cs new file mode 100644 index 0000000..8df2dd5 --- /dev/null +++ b/Data/UserOfTheDayRepository.cs @@ -0,0 +1,72 @@ +using System.Data; +using Dapper; +using Microsoft.Extensions.Configuration; +using Npgsql; +using UserOfTheDayBot.Model; + +namespace UserOfTheDayBot.Data; + +public interface IUserOfTheDayRepository +{ + Task RecordUserOfTheDayAsync(long chatId, long userId, DateTime date, UserOfTheDayType type); + Task IsUserOfTheDaySelectedAsync(long chatId, DateTime date, UserOfTheDayType type); + Task IsUserAlreadySelectedAsync(long chatId, DateTime date, long userId); + Task> GetUserStatisticsAsync(long chatId); +} + + public class UserOfTheDayRepository : IUserOfTheDayRepository + { + private readonly string _connectionString; + + public UserOfTheDayRepository(IConfiguration configuration) + { + _connectionString = configuration["DatabaseSettings:ConnectionString"]; + } + + public async Task RecordUserOfTheDayAsync(long chatId, long userId, DateTime date, UserOfTheDayType type) + { + using (IDbConnection db = new NpgsqlConnection(_connectionString)) + { + string query = "INSERT INTO user_of_the_day (chat_id, user_id, date, type) VALUES (@ChatId, @UserId, @Date, @Type)"; + await db.ExecuteAsync(query, new { ChatId = chatId, UserId = userId, Date = date, Type = type }); + } + } + + public async Task IsUserOfTheDaySelectedAsync(long chatId, DateTime date, UserOfTheDayType type) + { + using (IDbConnection db = new NpgsqlConnection(_connectionString)) + { + string query = "SELECT COUNT(1) FROM user_of_the_day WHERE chat_id = @ChatId AND date = @Date AND type = @Type"; + return await db.ExecuteScalarAsync(query, new { ChatId = chatId, Date = date, Type = type }); + } + } + + public async Task IsUserAlreadySelectedAsync(long chatId, DateTime date, long userId) + { + using (IDbConnection db = new NpgsqlConnection(_connectionString)) + { + string query = "SELECT COUNT(1) FROM user_of_the_day WHERE chat_id = @ChatId AND date = @Date AND user_id = @UserId"; + return await db.ExecuteScalarAsync(query, new { ChatId = chatId, Date = date, UserId = userId }); + } + } + + public async Task> GetUserStatisticsAsync(long chatId) + { + using (IDbConnection db = new NpgsqlConnection(_connectionString)) + { + string query = @"SELECT u.user_id, u.user_name, + SUM(CASE WHEN ud.type = 0 THEN 1 ELSE 0 END) AS user_of_the_day_count, + SUM(CASE WHEN ud.type = 1 THEN 1 ELSE 0 END) AS loser_of_the_day_count + FROM user_of_the_day ud + JOIN users u ON ud.user_id = u.user_id AND ud.chat_id = u.chat_id + WHERE ud.chat_id = @ChatId + GROUP BY u.user_id, u.user_name"; + + var results = await db.QueryAsync<(long userId, string userName, int userOfTheDayCount, int loserOfTheDayCount)>(query, new { ChatId = chatId }); + return results.ToDictionary( + row => (row.userId, row.userName), + row => (row.userOfTheDayCount, row.loserOfTheDayCount) + ); + } + } + } \ No newline at end of file diff --git a/Data/UserRepository.cs b/Data/UserRepository.cs new file mode 100644 index 0000000..6b3fb29 --- /dev/null +++ b/Data/UserRepository.cs @@ -0,0 +1,41 @@ +using System.Data; +using Dapper; +using Microsoft.Extensions.Configuration; +using Npgsql; + +namespace UserOfTheDayBot.Data; + +public interface IUserRepository +{ + Task RegisterUserAsync(long chatId, long userId, string userName); + Task> GetUsersWithNamesAsync(long chatId); +} + +public class UserRepository : IUserRepository +{ + private readonly string _connectionString; + + public UserRepository(IConfiguration configuration) + { + _connectionString = configuration["DatabaseSettings:ConnectionString"]; + } + + + public async Task RegisterUserAsync(long chatId, long userId, string userName) + { + using (IDbConnection db = new NpgsqlConnection(_connectionString)) + { + string query = "INSERT INTO users (chat_id, user_id, user_name) VALUES (@ChatId, @UserId, @UserName) ON CONFLICT (chat_id, user_id) DO NOTHING"; + return await db.ExecuteAsync(query, new { ChatId = chatId, UserId = userId, UserName = userName }); + } + } + + public async Task> GetUsersWithNamesAsync(long chatId) + { + using (IDbConnection db = new NpgsqlConnection(_connectionString)) + { + string query = "SELECT user_id, user_name FROM users WHERE chat_id = @ChatId"; + return await db.QueryAsync<(long, string)>(query, new { ChatId = chatId }); + } + } +} \ No newline at end of file diff --git a/Model/UserOfTheDayType.cs b/Model/UserOfTheDayType.cs new file mode 100644 index 0000000..21d6a54 --- /dev/null +++ b/Model/UserOfTheDayType.cs @@ -0,0 +1,7 @@ +namespace UserOfTheDayBot.Model; + +public enum UserOfTheDayType +{ + UserOfTheDay = 0, + LoserOfTheDay = 1 +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 29bab0f..e51aea6 100644 --- a/Program.cs +++ b/Program.cs @@ -6,89 +6,37 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Dapper; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Npgsql; +using UserOfTheDayBot.Data; +using UserOfTheDayBot.Services; -class Program +public class Program { - private static string botToken = "7814259349:AAEasTnDpX5s5PrQcR5ihI9pOsmp2Ocv-m0"; // Укажите токен вашего бота - private static string connectionString = "Host=host.docker.internal;Port=5432;Database=telegram_bot;Username=postgres;Password=postgres"; - - private static readonly TelegramBotClient botClient = new TelegramBotClient(botToken); - - static async Task Main(string[] args) + public static async Task Main(string[] args) { - Console.WriteLine("Bot is running..."); - - // Управление токеном завершения - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (_, _) => cts.Cancel(); - - botClient.StartReceiving(UpdateHandler, ErrorHandler, cancellationToken: cts.Token); - - Console.WriteLine("Press Ctrl+C to exit..."); - await Task.Delay(Timeout.Infinite, cts.Token); - } - - private static async Task UpdateHandler(ITelegramBotClient bot, Update update, CancellationToken cancellationToken) - { - Console.WriteLine("Received an message"); - - if (update.Type == UpdateType.Message && update.Message?.Text != null) - { - var message = update.Message; - - if (message.Text.StartsWith("/help")) + var host = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((context, config) => { - await bot.SendTextMessageAsync(message.Chat.Id, "Уйди. Попробуйте /reg", cancellationToken: cancellationToken); - } - else if (message.Text.StartsWith("/reg")) + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + config.AddEnvironmentVariables(); // Добавление переменных среды + }) + .ConfigureServices((context, services) => { - await RegisterUser(message, cancellationToken); - } - else - { - await bot.SendTextMessageAsync(message.Chat.Id, "Неизвестная команда. Попробуйте /reg", cancellationToken: cancellationToken); - } - } - } + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - private static async Task RegisterUser(Message message, CancellationToken cancellationToken) - { - try - { - using (IDbConnection db = new NpgsqlConnection(connectionString)) - { - string query = "INSERT INTO users (chat_id, user_id, user_name) VALUES (@ChatId, @UserId, @UserName) ON CONFLICT (chat_id, user_id) DO NOTHING"; - - var parameters = new + // TelegramBotClient зависит от IConfiguration для получения токена + services.AddSingleton(provider => { - ChatId = message.Chat.Id, - UserId = message.From.Id, - UserName = message.From.FirstName - }; - - int rowsAffected = await db.ExecuteAsync(query, parameters); - - if (rowsAffected > 0) - { - await botClient.SendTextMessageAsync(message.Chat.Id, "Вы успешно зарегистрированы!", cancellationToken: cancellationToken); - } - else - { - await botClient.SendTextMessageAsync(message.Chat.Id, "Вы уже зарегистрированы.", cancellationToken: cancellationToken); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"Error: {ex.Message}"); - await botClient.SendTextMessageAsync(message.Chat.Id, "Произошла ошибка при регистрации. Попробуйте позже.", cancellationToken: cancellationToken); - } - } - - private static Task ErrorHandler(ITelegramBotClient bot, Exception exception, CancellationToken cancellationToken) - { - Console.WriteLine($"Error: {exception.Message}"); - return Task.CompletedTask; + var configuration = provider.GetRequiredService(); + var botToken = configuration["BotSettings:BotToken"]; + return new TelegramBotClient(botToken); + }); + }) + .Build(); } } diff --git a/Services/BotService.cs b/Services/BotService.cs new file mode 100644 index 0000000..c15b6f9 --- /dev/null +++ b/Services/BotService.cs @@ -0,0 +1,187 @@ +using System.Security.Cryptography; +using Microsoft.Extensions.Configuration; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using UserOfTheDayBot.Data; +using UserOfTheDayBot.Model; + +namespace UserOfTheDayBot.Services; + +public class BotService + { + private readonly ITelegramBotClient _botClient; + private readonly IUserRepository _userRepository; + private readonly IUserOfTheDayRepository _userOfTheDayRepository; + + public BotService(IConfiguration configuration, IUserRepository userRepository, IUserOfTheDayRepository userOfTheDayRepository) + { + _botClient = new TelegramBotClient(configuration["BotSettings:BotToken"] ?? Environment.GetEnvironmentVariable("BotSettings_BotToken")); + _userRepository = userRepository; + _userOfTheDayRepository = userOfTheDayRepository; + } + + public async Task HandleUpdateAsync(Update update, CancellationToken cancellationToken) + { + if (update.Type == UpdateType.Message && update.Message?.Text != null) + { + var message = update.Message; + + if (message.Text.StartsWith("/help")) + { + await _botClient.SendTextMessageAsync(message.Chat.Id, "Уйди. Попробуйте /reg", cancellationToken: cancellationToken); + } + else if (message.Text.StartsWith("/reg")) + { + await RegisterUserAsync(message, cancellationToken); + } + else if (message.Text.StartsWith("/useroftheday")) + { + await HandleUserOfTheDayCommandAsync(message, UserOfTheDayType.UserOfTheDay, cancellationToken); + } + else if (message.Text.StartsWith("/loser")) + { + await HandleUserOfTheDayCommandAsync(message, UserOfTheDayType.LoserOfTheDay, cancellationToken); + } + else if (message.Text.StartsWith("/stat")) + { + await HandleStatCommandAsync(message, cancellationToken); + } + else + { + await _botClient.SendTextMessageAsync(message.Chat.Id, "Неизвестная команда. Попробуйте /reg", cancellationToken: cancellationToken); + } + } + } + + private async Task RegisterUserAsync(Message message, CancellationToken cancellationToken) + { + try + { + int rowsAffected = await _userRepository.RegisterUserAsync(message.Chat.Id, message.From.Id, message.From.FirstName); + + if (rowsAffected > 0) + { + await _botClient.SendTextMessageAsync(message.Chat.Id, "Вы успешно зарегистрированы!", cancellationToken: cancellationToken); + } + else + { + await _botClient.SendTextMessageAsync(message.Chat.Id, "Вы уже зарегистрированы.", cancellationToken: cancellationToken); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + await _botClient.SendTextMessageAsync(message.Chat.Id, "Произошла ошибка при регистрации. Попробуйте позже.", cancellationToken: cancellationToken); + } + } + + private async Task HandleUserOfTheDayCommandAsync(Message message, UserOfTheDayType type, CancellationToken cancellationToken) + { + try + { + var today = DateTime.UtcNow.Date; + var chatId = message.Chat.Id; + + if (await _userOfTheDayRepository.IsUserOfTheDaySelectedAsync(chatId, today, type)) + { + var responseMessage = type == UserOfTheDayType.UserOfTheDay ? "Пользователь дня уже выбран." : "Неудачник дня уже выбран."; + await _botClient.SendTextMessageAsync(chatId, responseMessage, cancellationToken: cancellationToken); + return; + } + + var selectedUser = await SelectUserOfTheDayAsync(chatId, today, type); + + if (selectedUser.HasValue) + { + var responseMessage = type == UserOfTheDayType.UserOfTheDay ? "Пользователь дня" : "Неудачник дня"; + await _botClient.SendTextMessageAsync(chatId, $"{responseMessage}: {selectedUser.Value}", cancellationToken: cancellationToken); + } + else + { + await _botClient.SendTextMessageAsync(chatId, "В чате нет зарегистрированных пользователей.", cancellationToken: cancellationToken); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + await _botClient.SendTextMessageAsync(message.Chat.Id, "Произошла ошибка при выборе пользователя дня. Попробуйте позже.", cancellationToken: cancellationToken); + } + } + + private async Task SelectUserOfTheDayAsync(long chatId, DateTime date, UserOfTheDayType type) + { + var users = (await _userRepository.GetUsersWithNamesAsync(chatId)).ToList(); + + if (users.Any()) + { + using (var rng = RandomNumberGenerator.Create()) + { + while (users.Count > 0) + { + var randomIndex = GetRandomIndex(users.Count, rng); + var selectedUser = users[randomIndex]; + + if (!await _userOfTheDayRepository.IsUserAlreadySelectedAsync(chatId, date, selectedUser.userId)) + { + await _userOfTheDayRepository.RecordUserOfTheDayAsync(chatId, selectedUser.userId, date, type); + return selectedUser.userId; + } + + users.RemoveAt(randomIndex); + } + } + } + + return null; + } + + private async Task HandleStatCommandAsync(Message message, CancellationToken cancellationToken) + { + try + { + var chatId = message.Chat.Id; + var stats = await _userOfTheDayRepository.GetUserStatisticsAsync(chatId); + + if (stats.Any()) + { + var userOfTheDaySection = "Пользователь дня:\n"; + var loserOfTheDaySection = "Неудачник дня:\n"; + + var sortedUserOfTheDay = stats.OrderByDescending(x => x.Value.userOfTheDayCount); + var sortedLoserOfTheDay = stats.OrderByDescending(x => x.Value.loserOfTheDayCount); + + foreach (var stat in sortedUserOfTheDay) + { + userOfTheDaySection += $"{stat.Key.userName}: {stat.Value.userOfTheDayCount} раз(а)\n"; + } + + foreach (var stat in sortedLoserOfTheDay) + { + loserOfTheDaySection += $"{stat.Key.userName}: {stat.Value.loserOfTheDayCount} раз(а)\n"; + } + + var response = userOfTheDaySection + "\n" + loserOfTheDaySection; + + await _botClient.SendTextMessageAsync(chatId, response, cancellationToken: cancellationToken); + } + else + { + await _botClient.SendTextMessageAsync(chatId, "Нет данных о пользователях.", cancellationToken: cancellationToken); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + await _botClient.SendTextMessageAsync(message.Chat.Id, "Произошла ошибка при получении статистики. Попробуйте позже.", cancellationToken: cancellationToken); + } + } + + private static int GetRandomIndex(int max, RandomNumberGenerator rng) + { + var buffer = new byte[4]; + rng.GetBytes(buffer); + var randomValue = BitConverter.ToUInt32(buffer, 0); + return (int)(randomValue % max); + } + } diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..84051af --- /dev/null +++ b/appsettings.json @@ -0,0 +1,8 @@ +{ + "BotSettings": { + "BotToken": "7571336725:AAGkzgAricQPWWsAinDCDKZivomXXy36Qpo" + }, + "DatabaseSettings": { + "ConnectionString": "Host=host.docker.internal;Port=5432;Database=telegram_bot;Username=postgres;Password=postgres" + } +} \ No newline at end of file