diff --git a/TelegramSearchBot.Test/Service/Tools/TodoToolServiceTests.cs b/TelegramSearchBot.Test/Service/Tools/TodoToolServiceTests.cs new file mode 100644 index 00000000..29307ebc --- /dev/null +++ b/TelegramSearchBot.Test/Service/Tools/TodoToolServiceTests.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TelegramSearchBot.Service.Tools; +using TelegramSearchBot.Model; +using Xunit; +using Moq; +using TelegramSearchBot.Interface; + +namespace TelegramSearchBot.Test.Service.Tools +{ + public class TodoToolServiceTests + { + private readonly Mock _mockSendMessageService; + private readonly TodoToolService _todoToolService; + + public TodoToolServiceTests() + { + _mockSendMessageService = new Mock(); + _todoToolService = new TodoToolService(_mockSendMessageService.Object, null); + } + + [Fact] + public async Task SendTodoToGroup_ValidParameters_Success() + { + // Arrange + long chatId = -123456789; // Group chat ID + string title = "Test Todo"; + string description = "This is a test todo item"; + string priority = "high"; + string dueDate = "2025-12-31"; + + // Act + var result = await _todoToolService.SendTodoToGroup(chatId, title, description, priority, dueDate); + + // Assert + Assert.Contains("✅", result); + Assert.Contains(title, result); + Assert.Contains(chatId.ToString(), result); + + // Verify SendMessage was called + _mockSendMessageService.Verify(x => x.SendMessage(It.Is(s => s.Contains(title)), chatId), Times.Once); + } + + [Fact] + public async Task SendTodoToGroup_PrivateChatId_LogsWarning() + { + // Arrange + long chatId = 123456789; // Private chat ID (positive) + string title = "Test Todo"; + string description = "This is a test todo item"; + + // Act + var result = await _todoToolService.SendTodoToGroup(chatId, title, description); + + // Assert + Assert.Contains("✅", result); // Should still work but with warning + _mockSendMessageService.Verify(x => x.SendMessage(It.Is(s => s.Contains(title)), chatId), Times.Once); + } + + [Fact] + public async Task SendTodoToGroup_InvalidPriority_DefaultsToMedium() + { + // Arrange + long chatId = -123456789; + string title = "Test Todo"; + string description = "This is a test todo item"; + string invalidPriority = "invalid"; + + // Act + var result = await _todoToolService.SendTodoToGroup(chatId, title, description, invalidPriority); + + // Assert + Assert.Contains("✅", result); + _mockSendMessageService.Verify(x => x.SendMessage(It.Is(s => s.Contains("MEDIUM")), chatId), Times.Once); + } + + [Fact] + public async Task SendQuickTodo_ValidParameters_Success() + { + // Arrange + long chatId = -123456789; + string message = "Quick todo message"; + + // Act + var result = await _todoToolService.SendQuickTodo(chatId, message); + + // Assert + Assert.Contains("✅", result); + Assert.Contains(chatId.ToString(), result); + + // Verify SendMessage was called with formatted message + _mockSendMessageService.Verify(x => x.SendMessage(It.Is(s => s.Contains("📋") && s.Contains("TODO")), chatId), Times.Once); + } + + [Fact] + public async Task SendTodoToGroup_SendMessageFails_ReturnsErrorMessage() + { + // Arrange + long chatId = -123456789; + string title = "Test Todo"; + string description = "This is a test todo item"; + + _mockSendMessageService + .Setup(x => x.SendMessage(It.IsAny(), chatId)) + .ThrowsAsync(new Exception("Telegram API error")); + + // Act + var result = await _todoToolService.SendTodoToGroup(chatId, title, description); + + // Assert + Assert.Contains("❌", result); + Assert.Contains("Failed to send todo", result); + Assert.Contains("Telegram API error", result); + } + } +} \ No newline at end of file diff --git a/TelegramSearchBot/Service/Tools/TodoToolService.cs b/TelegramSearchBot/Service/Tools/TodoToolService.cs new file mode 100644 index 00000000..303cff1c --- /dev/null +++ b/TelegramSearchBot/Service/Tools/TodoToolService.cs @@ -0,0 +1,154 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using TelegramSearchBot.Attributes; +using TelegramSearchBot.Interface; +using TelegramSearchBot.Model; + +namespace TelegramSearchBot.Service.Tools +{ + /// + /// LLM tool for sending todo messages to group chats + /// + [Injectable(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient)] + public class TodoToolService + { + private readonly ISendMessageService _sendMessageService; + private readonly ILogger _logger; + + public TodoToolService(ISendMessageService sendMessageService, ILogger logger) + { + _sendMessageService = sendMessageService; + _logger = logger; + } + + /// + /// Send a todo message to a group chat + /// + /// Target group chat ID + /// Todo title/subject + /// Detailed description of the todo + /// Priority level (low, medium, high, urgent) + /// Optional due date for the todo + /// Tool execution context + /// Confirmation message + [McpTool("Send a todo message to a group chat with structured information including title, description, priority, and optional due date.")] + public async Task SendTodoToGroup( + [McpParameter("Target group chat ID to send the todo message to")] + long chatId, + + [McpParameter("Title or subject of the todo item")] + string title, + + [McpParameter("Detailed description of the todo item")] + string description, + + [McpParameter("Priority level (low, medium, high, urgent)", IsRequired = false)] + string priority = "medium", + + [McpParameter("Optional due date for the todo item (format: YYYY-MM-DD or YYYY-MM-DD HH:MM)", IsRequired = false)] + string dueDate = null, + + ToolContext toolContext = null) + { + try + { + // Validate chat ID is a group chat (negative chat IDs are groups/channels) + if (chatId > 0) + { + _logger.LogWarning("TodoToolService: Chat ID {ChatId} appears to be a private chat, not a group", chatId); + } + + // Validate priority + priority = priority?.ToLowerInvariant() ?? "medium"; + if (!new[] { "low", "medium", "high", "urgent" }.Contains(priority)) + { + priority = "medium"; + } + + // Build todo message + var message = BuildTodoMessage(title, description, priority, dueDate); + + // Send message to group + await _sendMessageService.SendMessage(message, chatId); + + _logger.LogInformation("TodoToolService: Successfully sent todo to chat {ChatId}: {Title}", chatId, title); + + return $"✅ Todo item '{title}' has been sent to group {chatId} with {priority} priority."; + } + catch (Exception ex) + { + _logger.LogError(ex, "TodoToolService: Failed to send todo to chat {ChatId}: {Title}", chatId, title); + return $"❌ Failed to send todo item '{title}' to group {chatId}: {ex.Message}"; + } + } + + /// + /// Send a quick todo message with minimal parameters + /// + /// Target group chat ID + /// Todo message content + /// Tool execution context + /// Confirmation message + [McpTool("Send a quick todo message to a group chat with minimal formatting")] + public async Task SendQuickTodo( + [McpParameter("Target group chat ID to send the todo message to")] + long chatId, + + [McpParameter("Quick todo message content")] + string message, + + ToolContext toolContext = null) + { + try + { + // Validate chat ID is a group chat (negative chat IDs are groups/channels) + if (chatId > 0) + { + _logger.LogWarning("TodoToolService: Chat ID {ChatId} appears to be a private chat, not a group", chatId); + } + + // Build quick todo message + var formattedMessage = $"📋 **TODO**\n\n{message}"; + + // Send message to group + await _sendMessageService.SendMessage(formattedMessage, chatId); + + _logger.LogInformation("TodoToolService: Successfully sent quick todo to chat {ChatId}", chatId); + + return $"✅ Quick todo has been sent to group {chatId}."; + } + catch (Exception ex) + { + _logger.LogError(ex, "TodoToolService: Failed to send quick todo to chat {ChatId}", chatId); + return $"❌ Failed to send quick todo to group {chatId}: {ex.Message}"; + } + } + + private string BuildTodoMessage(string title, string description, string priority, string dueDate) + { + var priorityEmojis = new Dictionary + { + { "low", "🟢" }, + { "medium", "🟡" }, + { "high", "🟠" }, + { "urgent", "🔴" } + }; + + var priorityEmoji = priorityEmojis.TryGetValue(priority, out var emoji) ? emoji : "🟡"; + + var message = $"📋 **TODO: {title}** {priorityEmoji}\n\n"; + message += $"**Priority:** {priority.ToUpperInvariant()}\n\n"; + message += $"**Description:**\n{description}\n"; + + if (!string.IsNullOrEmpty(dueDate)) + { + message += $"\n**Due Date:** {dueDate}\n"; + } + + message += $"\n*Created: {DateTimeOffset.Now:yyyy-MM-dd HH:mm})*"; + + return message; + } + } +} \ No newline at end of file