diff --git a/.gitignore b/.gitignore index a9d34b8..6733330 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -notes.md \ No newline at end of file +notes.md +backup.json \ No newline at end of file diff --git a/Code.cs b/Code.cs index 2005f38..b8a9c18 100644 --- a/Code.cs +++ b/Code.cs @@ -25,26 +25,9 @@ using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -/* -Todo +#region Data Models - -{ - platform-userId: { - username: string - tasks: [ - name - completed? - timestamp added - timestamp completed - ], - totalIncompleteCount: uint - totalCompletedCount: uint - } - task-data -} -*/ -class Task +public class Task { public string Name; public bool Completed; @@ -62,7 +45,7 @@ class Task } } -class UserData +public class UserData { public string username; public List tasks; @@ -99,30 +82,421 @@ public class Response : Response } } -public class CPHInline +#endregion + +#region Helper Classes + +public static class TaskHelpers { - private int characterLimit = 450; - private Dictionary taskData = new Dictionary(); - public char[] separators = + public static readonly char[] Separators = { '|', ',', ';' }; + public const int CharacterLimit = 450; + + public static List SplitTasks(string tasks) { - '|', - ',', - ';' - }; - public void Init() - { - // task list - string taskDataString = CPH.GetGlobalVar("rython-task-bot", true); - Dictionary taskListData = JsonConvert.DeserializeObject>(taskDataString); - this.taskData = taskListData; - Cleanup(false); + return tasks.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); } - // Send changes to frontend - private void Broadcast(T t, string? key = null) + public static int GetTaskIndex(List tasks, string input) { - // INDEX MUST BE TRUE INDEX - string json = JsonConvert.SerializeObject(new { source = "rython-task-bot", id = key ?? GetKey(), body = t, username = GetUsername() }); + if (int.TryParse(input, out int n)) + { + int index = n - 1; + return (index >= 0 && index < tasks.Count) ? index : -1; + } + return tasks.FindIndex(t => t.Name.Equals(input, StringComparison.OrdinalIgnoreCase)); + } + + public static List ParseTasksInput(string input, Func getFocusedTask) + { + input = input.Trim(); + bool IsSpaceSeparatedInts = Regex.IsMatch(input, @"^\d+(\s+\d+)*$"); + if (IsSpaceSeparatedInts) + { + return input.Split(' ').ToList(); + } + + var splittedInput = input.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); + if (splittedInput.Count == 0) + { + int focusedTaskIndex = getFocusedTask() + 1; + return new List { focusedTaskIndex.ToString() }; + } + return splittedInput; + } + + public static List ParseUndoneInput(string input, List userTasks) + { + input = input.Trim(); + bool IsSpaceSeparatedInts = Regex.IsMatch(input, @"^\d+(\s+\d+)*$"); + if (IsSpaceSeparatedInts) + { + return input.Split(' ').ToList(); + } + + var splittedInput = input.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); + if (splittedInput.Count == 0) + { + var completedTasks = userTasks.Where(t => t.Completed); + if (completedTasks.Count() == 1) + { + int soloCompletedTaskIndex = userTasks.FindIndex(t => t.Completed) + 1; + return new List { soloCompletedTaskIndex.ToString() }; + } + } + else + { + return splittedInput; + } + return new List(); + } + + public static Response<(int, string)> ParseEditInput(string rawInput, int focusedTaskIndex) + { + bool validEditInput = true; + rawInput = rawInput.Trim(); + bool hasFocusedTask = focusedTaskIndex > -1; + string[] spaceSeparated = rawInput.Split(new[] { ' ' }, 2); + + if (spaceSeparated.Length < 2) + { + validEditInput = false; + if (!hasFocusedTask) + { + return new Response<(int, string)>(false, default, "Error: Not enough arguments, try !edit "); + } + } + + string numberString = spaceSeparated[0]; + string newTask = spaceSeparated.Length > 1 ? spaceSeparated[1] : rawInput; + + if (!int.TryParse(numberString, out int index)) + { + validEditInput = false; + if (!hasFocusedTask) + { + return new Response<(int, string)>(false, default, "Error: Not a valid argument, try !edit "); + } + } + + if (validEditInput) + { + return new Response<(int, string)>(true, (index - 1, newTask), null); + } + return new Response<(int, string)>(true, (focusedTaskIndex, rawInput), null); + } +} + +public static class MessageBuilder +{ + public static string BuildAddResponseMessage(List added, List<(string, string)> failed) + { + if (added.Count > 0 && failed.Count == 0) + { + string response = $"Added: {String.Join(" | ", added)}"; + return response.Length > TaskHelpers.CharacterLimit ? "All the tasks have been added!" : response; + } + if (added.Count == 0 && failed.Count == 1) + { + return failed[0].Item2; + } + if (added.Count == 0 && failed.Count > 1) + { + string response = $"Failed: {String.Join(", ", failed.Select(f => f.Item1))}"; + return response.Length > TaskHelpers.CharacterLimit ? "None of the tasks successfully added" : response; + } + if (added.Count > 0 && failed.Count > 0) + { + string response = $"Added: {String.Join(" | ", added)} | Failed: {String.Join(", ", failed.Select(f => f.Item1))}"; + return response.Length > TaskHelpers.CharacterLimit ? "Some tasks successful but some failed :p" : response; + } + return "No tasks to add"; + } + + public static string BuildLogResponseMessage(List added, List<(string, string)> failed) + { + return BuildAddResponseMessage(added, failed); + } + + public static string BuildRemoveMessage(List tasksRemoved, List tasksFailedToRemove) + { + if (tasksFailedToRemove.Count == 0) + { + return "Removed task(s)!"; + } + return $"Failed to remove: {String.Join(", ", tasksFailedToRemove)}"; + } + + public static string BuildCompletedMessage(List tasksCompleted, List tasksFailedToComplete) + { + if (tasksFailedToComplete.Count == 0 && tasksCompleted.Count == 1) + { + return "Task completed!"; + } + if (tasksFailedToComplete.Count == 0) + { + return "Completed all task(s) specified!"; + } + return $"Failed to complete: {String.Join(", ", tasksFailedToComplete)}"; + } + + public static string BuildUndoneMessage(List tasksCompleted, List tasksFailedToComplete) + { + if (tasksFailedToComplete.Count == 0 && tasksCompleted.Count == 1) + { + return "Task marked as incomplete!"; + } + if (tasksFailedToComplete.Count == 0) + { + return "Task(s) marked as incomplete!"; + } + return $"Failed to parse: {String.Join(", ", tasksFailedToComplete)}"; + } +} + +#endregion + +#region Task Operations + +public class TaskOperations +{ + private Dictionary taskData; + private readonly Action broadcast; + private readonly Func getKey; + private readonly Func getKeyByUsername; + private readonly Func getUsername; + + public TaskOperations( + Dictionary taskData, + Action broadcast, + Func getKey, + Func getKeyByUsername, + Func getUsername) + { + this.taskData = taskData; + this.broadcast = broadcast; + this.getKey = getKey; + this.getKeyByUsername = getKeyByUsername; + this.getUsername = getUsername; + } + + public void SetTaskData(Dictionary data) => this.taskData = data; + public Dictionary GetTaskData() => this.taskData; + + public List ListUserTasks(string userKey = null) + { + var emptyList = new List(); + if (taskData == null || taskData.Count == 0) + return emptyList; + + string key = userKey ?? getKey(); + if (!taskData.TryGetValue(key, out var userData)) + return emptyList; + + return userData.tasks.Count == 0 ? emptyList : taskData[key].tasks; + } + + public int GetFocusedTask() + { + var userTasks = ListUserTasks(); + var incompleteTasks = userTasks.Where(t => !t.Completed); + if (incompleteTasks.Count() == 1) + { + return userTasks.FindIndex(t => !t.Completed); + } + return userTasks.FindIndex(t => t.Focused); + } + + public Response<(int, string)> AddTask(string taskName, bool completed = false, bool focused = false) + { + taskName = taskName.Trim(); + if (string.IsNullOrEmpty(taskName)) + return new Response<(int, string)>(false, default, "Task cannot be empty"); + if (int.TryParse(taskName, out _)) + return new Response<(int, string)>(false, default, $"'{taskName}' cannot be a number"); + + string key = getKey(); + if (!taskData.ContainsKey(key)) + { + string username = getUsername(); + taskData.Add(key, new UserData(new List(), username)); + } + + if (taskData[key].tasks.Any(t => t.Name.Equals(taskName, StringComparison.OrdinalIgnoreCase) && !t.Completed)) + return new Response<(int, string)>(false, default, $"Error: Task '{taskName}' already exists"); + + taskData[key].username = getUsername(); + taskData[key].tasks.Add(new Task(taskName, completed, focused)); + int newIndex = taskData[key].tasks.Count - 1; + broadcast(new { mode = "add", task = taskName, completed = completed, focused = focused }, null); + return new Response<(int, string)>(true, (newIndex, taskName), null); + } + + public Response<(string, string)> EditTask(int index, string newTask) + { + var userTasks = ListUserTasks(); + if (userTasks.Count == 0) + return new Response<(string, string)>(false, default, "Error 404: Tasks not found"); + if (index >= userTasks.Count || index < 0) + return new Response<(string, string)>(false, default, "Error: Invalid task number"); + + bool taskAlreadyExists = userTasks.FindIndex(t => t.Name.Equals(newTask, StringComparison.OrdinalIgnoreCase)) > -1; + if (taskAlreadyExists) + return new Response<(string, string)>(false, default, "Error: Task already exists"); + + string oldName = userTasks[index].Name; + userTasks[index].Name = newTask; + SaveIntoTasks(userTasks); + broadcast(new { mode = "edit", index = index, task = newTask }, null); + return new Response<(string, string)>(true, (oldName, newTask), null); + } + + public Response<(int, string)> FocusOnTask(string rawInput) + { + bool containsSeparators = rawInput.IndexOfAny(TaskHelpers.Separators) >= 0; + if (containsSeparators) + return new Response<(int, string)>(false, default, "Cannot focus on multiple tasks"); + + var tasks = ListUserTasks(); + int indexByName = tasks.FindIndex(t => t.Name.Equals(rawInput, StringComparison.OrdinalIgnoreCase)); + + if (int.TryParse(rawInput, out int n)) + { + n = n - 1; + if (n < 0 || n >= tasks.Count) + return new Response<(int, string)>(false, default, "Index out of range."); + if (tasks[n].Completed) + return new Response<(int, string)>(false, default, "Cannot focus on completed task."); + + UnfocusAll(tasks); + tasks[n].Focused = true; + SaveIntoTasks(tasks); + broadcast(new { mode = "focus", index = n }, null); + return new Response<(int, string)>(true, (n, tasks[n].Name), null); + } + else if (indexByName > -1) + { + n = indexByName; + if (tasks[n].Completed) + return new Response<(int, string)>(false, default, "Cannot focus on completed task."); + + UnfocusAll(tasks); + tasks[n].Focused = true; + SaveIntoTasks(tasks); + broadcast(new { mode = "focus", index = n }, null); + return new Response<(int, string)>(true, (n, tasks[n].Name), null); + } + else + { + var response = AddTask(rawInput, false, true); + if (response.Success) + { + broadcast(new { mode = "focus", index = response.Data.Item1 }, null); + return new Response<(int, string)>(true, response.Data, null); + } + return new Response<(int, string)>(false, default, response.ErrorMsg); + } + } + + public void Unfocus() + { + var tasks = ListUserTasks(); + UnfocusAll(tasks); + SaveIntoTasks(tasks); + } + + private void UnfocusAll(List tasks) + { + for (int i = 0; i < tasks.Count; i++) + tasks[i].Focused = false; + } + + public void SaveIntoTasks(List tasks) + { + string key = getKey(); + taskData[key].username = getUsername(); + taskData[key].tasks = tasks; + } + + public void Cleanup(bool userOnly = false) + { + if (userOnly) + { + var userTasks = ListUserTasks(); + if (userTasks.Count == 0) + { + string userKey = getKey(); + taskData.Remove(userKey); + } + } + else + { + var keysToRemove = taskData.Where(item => item.Value.tasks.Count == 0).Select(item => item.Key).ToList(); + foreach (string key in keysToRemove) + taskData.Remove(key); + } + } + + public void RemoveUser(string key) + { + taskData.Remove(key); + } + + public void ClearAllTasks() + { + foreach (var item in taskData.Keys.ToList()) + taskData[item].tasks = new List(); + } + + public void ClearCompletedTasks() + { + foreach (var item in taskData.Keys.ToList()) + taskData[item].tasks = taskData[item].tasks.Where(t => !t.Completed).ToList(); + } + + public void ClearUserCompletedTasks(string key) + { + taskData[key].tasks = taskData[key].tasks.Where(t => !t.Completed).ToList(); + } + + public void FilterToStreamers(List streamerUsernames) + { + taskData = taskData + .Where(kvp => streamerUsernames.Any(s => string.Equals(s, kvp.Value.username, StringComparison.OrdinalIgnoreCase))) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } +} + +#endregion + +#region Main Command Handler + +public class CPHInline +{ + private Dictionary taskData = new Dictionary(); + private TaskOperations operations; + + public void Init() + { + string taskDataString = CPH.GetGlobalVar("rython-task-bot", true); + taskData = JsonConvert.DeserializeObject>(taskDataString) ?? new Dictionary(); + + operations = new TaskOperations( + taskData, + Broadcast, + () => GetKey(), + (username) => GetKey(username), + () => GetUsername() + ); + + operations.Cleanup(false); + SaveTasks(); + } + + #region Platform Helpers + + private void Broadcast(object body, string key) + { + string json = JsonConvert.SerializeObject(new { source = "rython-task-bot", id = key ?? GetKey(), body = body, username = GetUsername() }); CPH.WebsocketBroadcastJson(json); } @@ -130,43 +504,46 @@ public class CPHInline { TwitchUserInfo twitchInfo = CPH.TwitchGetBroadcaster(); var youtubeInfo = CPH.YouTubeGetBroadcaster(); - List usernames = new() - { - twitchInfo.UserName, - youtubeInfo.UserName - }; - return usernames; + return new List { twitchInfo.UserName, youtubeInfo.UserName }; } - private string GetUsername(string? key = null) + private string GetUsername(string key = null) { if (String.IsNullOrEmpty(key)) { CPH.TryGetArg("user", out string username); return username; } - else + return taskData[key].username; + } + + private string GetKey(string username = null) + { + if (!String.IsNullOrEmpty(username)) { - return this.taskData[key].username; + if (username[0] == '@') + username = username.Substring(1); + + foreach (var item in taskData) + { + if (item.Value.username.Equals(username, StringComparison.OrdinalIgnoreCase)) + return item.Key; + } + return ""; } - return ""; + CPH.TryGetArg("userType", out string platform); + CPH.TryGetArg("userId", out string userId); + return $"{platform}-{userId}"; } private void SaveTasks() { - string taskDataString = JsonConvert.SerializeObject(this.taskData); + taskData = operations.GetTaskData(); + string taskDataString = JsonConvert.SerializeObject(taskData); CPH.SetGlobalVar("rython-task-bot", taskDataString, true); } - private void SaveIntoTasks(List tasks) - { - string key = GetKey(); - this.taskData[key].username = GetUsername(); - this.taskData[key].tasks = tasks; - SaveTasks(); - } - private void Respond(string message) { CPH.TryGetArg("userType", out string platform); @@ -180,74 +557,12 @@ public class CPHInline CPH.TryGetArg("user", out string YTUser); CPH.SendYouTubeMessage($"@{YTUser} {message}"); break; - default: - // idk i don't stream elsewhere - break; } } - private List SplitTasks(string tasks) - { - return tasks.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); - } + #endregion - private string GetKey(string? username = null) - { - if (!String.IsNullOrEmpty(username)) - { - if (username[0] == '@') - { - username = username.Substring(1); - } - - // dig through task data, find username, case insensitive - foreach (var item in this.taskData) - { - if (item.Value.username.Equals(username, StringComparison.OrdinalIgnoreCase)) - { - return item.Key; - } - } - - return ""; - } - else - { - CPH.TryGetArg("userType", out string platform); - CPH.TryGetArg("userId", out string userId); - string key = $"{platform}-{userId}"; - return key; - } - } - - // ADD COMMAND - // <(true index, task name)> - private Response<(int, string)> AddTask(string taskName, bool completed = false, bool focused = false) - { - taskName = taskName.Trim(); - // Validation - if (string.IsNullOrEmpty(taskName)) - return new Response<(int, string)>(false, default, "Task cannot be empty"); - if (int.TryParse(taskName, out _)) - return new Response<(int, string)>(false, default, $"'{taskName}' cannot be a number"); - // Ensure user data exists - string key = GetKey(); - if (!this.taskData.ContainsKey(key)) - { - string username = GetUsername(); - this.taskData.Add(key, new UserData(new List(), username)); - } - - // Check for duplicate incomplete task - if (this.taskData[key].tasks.Any(t => t.Name.Equals(taskName, StringComparison.OrdinalIgnoreCase) && !t.Completed)) - return new Response<(int, string)>(false, default, $"Error: Task '{taskName}' already exists"); - // Add task - this.taskData[key].username = GetUsername(); - this.taskData[key].tasks.Add(new Task(taskName, completed, focused)); - int newIndex = this.taskData[key].tasks.Count - 1; // 1-based index for display - Broadcast(new { mode = "add", task = taskName, completed = completed, focused = focused }); - return new Response<(int, string)>(true, (newIndex, taskName), null); - } + #region Commands public bool HelpCommand() { @@ -258,427 +573,133 @@ public class CPHInline public bool AddCommand() { CPH.TryGetArg("rawInput", out string rawInput); - List taskStrings = SplitTasks(rawInput); + var taskStrings = TaskHelpers.SplitTasks(rawInput); if (taskStrings.Count == 0) { Respond("No tasks provided"); return false; } - List added = new(); - List<(string, string)> failed = new(); + var added = new List(); + var failed = new List<(string, string)>(); foreach (string taskString in taskStrings) { - var response = AddTask(taskString); + var response = operations.AddTask(taskString); if (response.Success) added.Add($"{response.Data.Item1 + 1}. {taskString.Trim()}"); else failed.Add((taskString, response.ErrorMsg)); } - // Save once after all tasks are processed - if (added.Count > 0) - { - SaveTasks(); - } - - // Build response - Respond(BuildAddResponseMessage(added, failed)); + if (added.Count > 0) SaveTasks(); + Respond(MessageBuilder.BuildAddResponseMessage(added, failed)); return true; } public bool LogCommand() { CPH.TryGetArg("rawInput", out string rawInput); - List taskStrings = SplitTasks(rawInput); + var taskStrings = TaskHelpers.SplitTasks(rawInput); if (taskStrings.Count == 0) { Respond("Error: No tasks provided"); return false; } - List logged = new(); - List<(string, string)> failed = new(); + var logged = new List(); + var failed = new List<(string, string)>(); foreach (string taskString in taskStrings) { - var response = AddTask(taskString, true, false); + var response = operations.AddTask(taskString, true, false); if (response.Success) logged.Add($"{response.Data.Item1 + 1}. {taskString.Trim()}"); else failed.Add((taskString, response.ErrorMsg)); } - // Save once after all tasks are processed - if (logged.Count > 0) - { - SaveTasks(); - } - - // Build response - Respond(BuildLogResponseMessage(logged, failed)); + if (logged.Count > 0) SaveTasks(); + Respond(MessageBuilder.BuildLogResponseMessage(logged, failed)); return true; } - private string BuildAddResponseMessage(List added, List<(string, string)> failed) - { - string response = "No tasks to add"; - if (added.Count > 0 && failed.Count == 0) - { - response = $"Added: {String.Join(" | ", added)}"; - if (response.Length > characterLimit) - { - response = "All the tasks have been added!"; - } - } - else if (added.Count == 0 && failed.Count == 1) - { - response = failed[0].Item2; // Single error, show the reason - } - else if (added.Count == 0 && failed.Count > 1) - { - response = $"Failed: {String.Join(", ", failed.Select(f => f.Item1))}"; - if (response.Length > characterLimit) - { - response = "None of the tasks successfully added"; - } - } - else if (added.Count > 0 && failed.Count > 0) - { - response = $"Added: {String.Join(" | ", added)} | Failed: {String.Join(", ", failed.Select(f => f.Item1))}"; - if (response.Length > characterLimit) - { - response = "Some tasks successful but some failed :p"; - } - } - - return response; - } - - private string BuildLogResponseMessage(List added, List<(string, string)> failed) - { - string response = "No tasks to add"; - if (added.Count > 0 && failed.Count == 0) - { - response = $"Added: {String.Join(" | ", added)}"; - if (response.Length > characterLimit) - { - response = "All the tasks have been added!"; - } - } - else if (added.Count == 0 && failed.Count == 1) - { - response = failed[0].Item2; // Single error, show the reason - } - else if (added.Count == 0 && failed.Count > 1) - { - response = $"Failed: {String.Join(", ", failed.Select(f => f.Item1))}"; - if (response.Length > characterLimit) - { - response = "None of the tasks successfully added"; - } - } - else if (added.Count > 0 && failed.Count > 0) - { - response = $"Added: {String.Join(" | ", added)} | Failed: {String.Join(", ", failed.Select(f => f.Item1))}"; - if (response.Length > characterLimit) - { - response = "Some tasks successful but some failed :p"; - } - } - - return response; - } - - // focus on existing task, or add a new task and focus on it - private Response<(int, string)> FocusOnTask(string rawInput) - { - // make sure there's no separators - bool containsSeparators = rawInput.IndexOfAny(separators) >= 0; - if (containsSeparators) - { - return new(false, default, "Cannot focus on multiple tasks"); - } - - List tasks = ListUserTasks(); - int IndexByName = tasks.FindIndex(t => t.Name.Equals(rawInput, StringComparison.OrdinalIgnoreCase)); - // existing task by name, new task, or index - if (int.TryParse(rawInput, out int n)) - { - n = n - 1; // true index - // IT'S A NUMBER - if (n < 0 || n >= tasks.Count) - { - return new(false, default, "Index out of range."); - } - else if (tasks[n].Completed) - { - // task already completed, cannot focus on it - return new(false, default, "Cannot focus on completed task."); - } - else - { - // unfocus on every single task - // then focus on that task - for (int i = 0; i < tasks.Count; i++) - { - tasks[i].Focused = false; - } - - tasks[n].Focused = true; - SaveIntoTasks(tasks); - Broadcast(new { mode = "focus", index = n }); - return new(true, (n, tasks[n].Name), null); - } - } - else if (IndexByName > -1) - { - // it's an existing task - n = IndexByName; - if (tasks[n].Completed) - { - // task already completed, cannot focus on it - return new(false, default, "Cannot focus on completed task."); - } - - for (int i = 0; i < tasks.Count; i++) - { - tasks[i].Focused = false; - } - - tasks[n].Focused = true; - SaveIntoTasks(tasks); - Broadcast(new { mode = "focus", index = n }); - return new(true, (n, tasks[n].Name), null); - } - else - { - // new task - var response = AddTask(rawInput, false, true); - if (response.Success) - { - Broadcast(new { mode = "focus", index = response.Data.Item1 }); - return new(true, (response.Data.Item1, response.Data.Item2), null); - } - else - { - return new(false, default, $"{response.ErrorMsg}"); - } - } - } - - // FOCUS COMMAND - // 1: focus on existing task by name (check if already focused) - // 2: focus on existing task by index (must be valid index that isn't already focused) - // 3: adding a new task by name public bool FocusCommand() { CPH.TryGetArg("rawInput", out string rawInput); - // make sure there's no separators - var AddOrSelectResponse = FocusOnTask(rawInput); - if (!AddOrSelectResponse.Success) - { - Respond(AddOrSelectResponse.ErrorMsg); - } + var response = operations.FocusOnTask(rawInput); + if (!response.Success) + Respond(response.ErrorMsg); else - { - Respond($"Current focused task: {AddOrSelectResponse.Data.Item1 + 1}. {AddOrSelectResponse.Data.Item2}"); - } - + Respond($"Current focused task: {response.Data.Item1 + 1}. {response.Data.Item2}"); + SaveTasks(); return true; } - // FOCUSED COMMAND - // Returns only the focused task public bool FocusedCommand() { - List userTasks = ListUserTasks(); - int focusedTaskIndex = GetFocusedTask(); + var userTasks = operations.ListUserTasks(); + int focusedTaskIndex = operations.GetFocusedTask(); if (focusedTaskIndex == -1) { Respond("You do not have a focused task."); return true; } - - var focusedTask = userTasks[focusedTaskIndex]; - Respond($"Current focused task: {focusedTaskIndex + 1}. {focusedTask.Name}"); + Respond($"Current focused task: {focusedTaskIndex + 1}. {userTasks[focusedTaskIndex].Name}"); return true; } - // return true index - private int GetFocusedTask() - { - List userTasks = ListUserTasks(); - var incompleteTasks = userTasks.Where(t => !t.Completed); - if (incompleteTasks.Count() == 1) - { - return userTasks.FindIndex(t => !t.Completed); - } - - return userTasks.FindIndex(t => t.Focused); - } - - // NEXT COMMAND - // get focused task -> mark it as complete -> focus on existing/new task public bool NextCommand() { - List userTasks = ListUserTasks(); - int focusedTaskIndex = GetFocusedTask(); + var userTasks = operations.ListUserTasks(); + int focusedTaskIndex = operations.GetFocusedTask(); if (focusedTaskIndex == -1) { Respond("Can't use !next command, select a task to complete using !done and/or add another task"); return false; } - // Focus on the task CPH.TryGetArg("rawInput", out string rawInput); - var FocusOnTaskResponse = FocusOnTask(rawInput); - if (!FocusOnTaskResponse.Success) + var focusResponse = operations.FocusOnTask(rawInput); + if (!focusResponse.Success) { - Respond(FocusOnTaskResponse.ErrorMsg); + Respond(focusResponse.ErrorMsg); return false; } - int newTaskIndex = FocusOnTaskResponse.Data.Item1; - string taskString = FocusOnTaskResponse.Data.Item2; - // mark it as complete string completedTaskName = userTasks[focusedTaskIndex].Name; userTasks[focusedTaskIndex].Completed = true; userTasks[focusedTaskIndex].Focused = false; - Broadcast(new { mode = "done", index = focusedTaskIndex }); - SaveIntoTasks(userTasks); - Respond($"Completed '{completedTaskName}'! Moving onto '({newTaskIndex + 1}) {taskString}'"); + Broadcast(new { mode = "done", index = focusedTaskIndex }, null); + operations.SaveIntoTasks(userTasks); + SaveTasks(); + Respond($"Completed '{completedTaskName}'! Moving onto '({focusResponse.Data.Item1 + 1}) {focusResponse.Data.Item2}'"); return true; } - // EDIT COMMAND public bool EditCommand() { CPH.TryGetArg("rawInput", out string rawInput); - // parse check input - var EditData = ParseEditInput(rawInput); - if (!EditData.Success) + var editData = TaskHelpers.ParseEditInput(rawInput, operations.GetFocusedTask()); + if (!editData.Success) { - Respond(EditData.ErrorMsg); + Respond(editData.ErrorMsg); return false; } - int taskIndex = EditData.Data.Item1; - string newTask = EditData.Data.Item2; - var result = EditTask(taskIndex, newTask); - if (!result.Success) - { - Respond(result.ErrorMsg); - } - else - { - Respond($"Task '{result.Data.Item1}' has been edited to '{result.Data.Item2}'"); - } - - // edit task + var result = operations.EditTask(editData.Data.Item1, editData.Data.Item2); + Respond(result.Success + ? $"Task '{result.Data.Item1}' has been edited to '{result.Data.Item2}'" + : result.ErrorMsg); + SaveTasks(); return true; } - private Response<(string, string)> EditTask(int index, string newTask) - { - List userTasks = ListUserTasks(); - // task data doesn't exist - if (userTasks.Count == 0) - { - return new(false, default, "Error 404: Tasks not found"); - } - - // out of range - if (index > userTasks.Count || index < 0) - { - return new(false, default, "Error: Invalid task number"); - } - - // incomplete task already exists - bool taskAlreadyExists = userTasks.FindIndex(t => t.Name.Equals(newTask, StringComparison.OrdinalIgnoreCase)) > -1; - if (taskAlreadyExists) - { - return new(false, default, "Error: Task already exists"); - } - - // userTasks - string oldName = userTasks[index].Name; - userTasks[index].Name = newTask; - SaveIntoTasks(userTasks); - Broadcast(new { mode = "edit", index = index, task = newTask }); - return new(true, (oldName, newTask), null); - } - - // simple: !edit - // Response<(int, string)> - private Response<(int, string)> ParseEditInput(string rawInput) - { - bool validEditInput = true; - rawInput = rawInput.Trim(); - int focusedTaskIndex = GetFocusedTask(); - bool hasFocusedTask = focusedTaskIndex > -1; - string[] spaceSeparated = rawInput.Split(new[] { ' ' }, 2); - if (spaceSeparated.Length < 2) - { - validEditInput = false; - if (!hasFocusedTask) - { - return new Response<(int, string)>(false, default, "Error: Not enough arguments, try !edit "); - } - } - - string numberString = spaceSeparated[0]; - string newTask = spaceSeparated[1]; - if (!int.TryParse(numberString, out int index)) - { - validEditInput = false; - if (!hasFocusedTask) - { - return new Response<(int, string)>(false, default, "Error: Not a valid argument, try !edit "); - } - } - - if (validEditInput) - { - return new Response<(int, string)>(true, (index - 1, newTask), null); - } - else - { - return new Response<(int, string)>(true, (focusedTaskIndex, rawInput), null); - } - } - - // CHECK COMMAND - private List ListUserTasks(string? userKey = null) - { - List emptyList = new List(); - if (this.taskData == null || this.taskData.Count == 0) - { - return emptyList; // return empty task list - } - - // user's data doesn't exist - string key = userKey ?? GetKey(); - if (!this.taskData.TryGetValue(key, out var userData)) - { - return emptyList; - } - - // 0 tasks - List tasks = userData.tasks; - if (tasks.Count == 0) - { - return emptyList; - } - - return this.taskData[key].tasks; - } - public bool CheckCommand() { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); - string? key = null; + string key = null; bool someoneElse = false; + if (!String.IsNullOrWhiteSpace(rawInput)) { key = GetKey(rawInput); @@ -687,33 +708,22 @@ public class CPHInline Respond("User not found"); return false; } - else - { - someoneElse = true; - } + someoneElse = true; } - List userTasks = ListUserTasks(key); - // task data doesn't exist + var userTasks = operations.ListUserTasks(key); if (userTasks.Count == 0) { Respond("404 Tasks not found"); return false; } - // JOIN THE TASKS INTO ONE MESSAGE - // edit however you like - var IncompleteTasks = userTasks.Select((t, index) => new { t, index }).Where(x => !x.t.Completed); - string message = String.Join(" | ", IncompleteTasks.Select(x => $"{x.index + 1}. {(x.t.Focused ? "(ongoing) " : "")}{x.t.Name}")); - if (someoneElse) - { - string theirUser = GetUsername(key); - message = $"{theirUser}'s tasks: {message}"; - } - else - { - message = $"{IncompleteTasks.Count()} tasks pending: {message}"; - } + var incompleteTasks = userTasks.Select((t, index) => new { t, index }).Where(x => !x.t.Completed); + string message = String.Join(" | ", incompleteTasks.Select(x => $"{x.index + 1}. {(x.t.Focused ? "(ongoing) " : "")}{x.t.Name}")); + + message = someoneElse + ? $"{GetUsername(key)}'s tasks: {message}" + : $"{incompleteTasks.Count()} tasks pending: {message}"; Respond(message); return true; @@ -723,8 +733,9 @@ public class CPHInline { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); - string? key = null; + string key = null; bool someoneElse = false; + if (!String.IsNullOrWhiteSpace(rawInput)) { key = GetKey(rawInput); @@ -733,33 +744,22 @@ public class CPHInline Respond("User not found"); return false; } - else - { - someoneElse = true; - } + someoneElse = true; } - List userTasks = ListUserTasks(key); - // task data doesn't exist + var userTasks = operations.ListUserTasks(key); if (userTasks.Count == 0) { Respond("404 Tasks not found"); return false; } - // JOIN THE TASKS INTO ONE MESSAGE - // edit however you like - var CompletedTasks = userTasks.Select((t, index) => new { t, index }).Where(x => x.t.Completed); - string message = String.Join(" | ", CompletedTasks.Select(x => $"{x.index + 1}. {x.t.Name}")); - if (someoneElse) - { - string theirUser = GetUsername(key); - message = $"{theirUser}'s tasks: {message}"; - } - else - { - message = $"Completed {CompletedTasks.Count()} tasks: {message}"; - } + var completedTasks = userTasks.Select((t, index) => new { t, index }).Where(x => x.t.Completed); + string message = String.Join(" | ", completedTasks.Select(x => $"{x.index + 1}. {x.t.Name}")); + + message = someoneElse + ? $"{GetUsername(key)}'s tasks: {message}" + : $"Completed {completedTasks.Count()} tasks: {message}"; Respond(message); return true; @@ -770,133 +770,43 @@ public class CPHInline CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); string separator = "; "; - string validSeparators = ";,|"; - if (rawInput.Length == 1 && validSeparators.Contains(rawInput)) - { + if (rawInput.Length == 1 && ";,|".Contains(rawInput)) separator = $"{rawInput} "; - } - List userTasks = ListUserTasks(); - string message = String.Join(separator, userTasks.Where((t, index) => !t.Completed).Select(task => task.Name)); + var userTasks = operations.ListUserTasks(); + string message = String.Join(separator, userTasks.Where(t => !t.Completed).Select(t => t.Name)); Respond(message); return true; } - // Helper method to get task index by number or name - private int GetTaskIndex(List tasks, string input) - { - if (int.TryParse(input, out int n)) - { - int index = n - 1; // Convert 1-based to 0-based - return (index >= 0 && index < tasks.Count) ? index : -1; - } - - return tasks.FindIndex(t => t.Name.Equals(input, StringComparison.OrdinalIgnoreCase)); - } - - // --- REMOVE TASK --- - private List ParseTasksInput(string input) - { - input = input.Trim(); - var tasksToRemove = new List(); - bool IsSpaceSeparatedInts = Regex.IsMatch(input, @"^\d+(\s+\d+)*$"); - if (IsSpaceSeparatedInts) - { - return input.Split(' ').ToList(); - } - - var splittedInput = input.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); - if (splittedInput.Count == 0) - { - // get focused task - int focusedTaskIndex = GetFocusedTask() + 1; - // return false index - return new List - { - focusedTaskIndex.ToString() - }; - } - else - { - return splittedInput; - } - } - - private string BuildRemoveMessage(List tasksRemoved, List tasksFailedToRemove) - { - // Build response - string removedString = String.Join(", ", tasksRemoved); - if (tasksFailedToRemove.Count == 0) - { - return $"Removed task(s)!"; - } - else - { - string failedString = String.Join(", ", tasksFailedToRemove); - return $"Failed to remove: {failedString}"; - } - } - - // remove user(s) if they don't have tasks - private void Cleanup(bool userOnly = false) - { - if (userOnly) - { - List userTasks = ListUserTasks(); - if (userTasks.Count == 0) - { - string userKey = GetKey(); - this.taskData.Remove(userKey); - } - } - else - { - List KeysToRemove = new List(); - foreach (var item in this.taskData) - { - if (item.Value.tasks.Count == 0) - { - KeysToRemove.Add(item.Key); - } - } - - foreach (string key in KeysToRemove) - { - this.taskData.Remove(key); - } - } - - SaveTasks(); - } - public bool RemoveCommand() { CPH.TryGetArg("rawInput", out string rawInput); - List userTasks = ListUserTasks(); + var userTasks = operations.ListUserTasks(); if (userTasks.Count == 0) { Respond("Error 404: Tasks not found"); return false; } - List tasksToBeRemoved = ParseTasksInput(rawInput); + var tasksToBeRemoved = TaskHelpers.ParseTasksInput(rawInput, operations.GetFocusedTask); if (tasksToBeRemoved.Count == 0) { Respond("Error: Invalid input"); return false; } - List tasksRemoved = new List(); - List tasksFailedToRemove = new List(); - List taskIndices = new List(); - // Gather indices of tasks to be removed + var tasksRemoved = new List(); + var tasksFailedToRemove = new List(); + var taskIndices = new List(); + foreach (string task in tasksToBeRemoved) { - int index = GetTaskIndex(userTasks, task); + int index = TaskHelpers.GetTaskIndex(userTasks, task); if (index > -1 && !taskIndices.Contains(index)) { taskIndices.Add(index); - tasksRemoved.Add(userTasks[index].Name); // Track what we're removing + tasksRemoved.Add(userTasks[index].Name); } else { @@ -904,38 +814,30 @@ public class CPHInline } } - // Nothing valid to remove if (taskIndices.Count == 0) { - string failedString = String.Join(", ", tasksFailedToRemove); - Respond($"Failed to remove task(s): {failedString}"); + Respond($"Failed to remove task(s): {String.Join(", ", tasksFailedToRemove)}"); return false; } - // Remove from highest index to lowest to preserve indices foreach (int i in taskIndices.OrderByDescending(n => n)) { userTasks.RemoveAt(i); - Broadcast(new { mode = "remove", index = i }); + Broadcast(new { mode = "remove", index = i }, null); } - // Save data - SaveIntoTasks(userTasks); - Cleanup(true); - Respond(BuildRemoveMessage(tasksRemoved, tasksFailedToRemove)); + operations.SaveIntoTasks(userTasks); + operations.Cleanup(true); + SaveTasks(); + Respond(MessageBuilder.BuildRemoveMessage(tasksRemoved, tasksFailedToRemove)); return true; } - // admin delete - // !adel @user public bool AdminDelete() { CPH.TryGetArg("rawInput", out string rawInput); string[] spaceSeparated = rawInput.Split(new[] { ' ' }, 2); - if (spaceSeparated.Count() == 0) - { - return false; - } + if (spaceSeparated.Length == 0) return false; string user = spaceSeparated[0]; string key = GetKey(user); @@ -945,62 +847,41 @@ public class CPHInline return false; } - // fuck it, just remove all - this.taskData.Remove(key); + operations.RemoveUser(key); SaveTasks(); Respond("All of the user's tasks have been deleted"); - Broadcast(new { mode = "admindelete", id = key }); + Broadcast(new { mode = "admindelete", id = key }, null); return true; } - // done command - private string BuildCompletedMessage(List tasksCompleted, List tasksFailedToComplete) - { - // Build response - // string completedString = String.Join(", ", tasksCompleted); - if (tasksFailedToComplete.Count == 0 && tasksCompleted.Count == 1) - { - return "Task completed!"; - } - else if (tasksFailedToComplete.Count == 0) - { - return "Completed all task(s) specified!"; - } - else - { - string failedString = String.Join(", ", tasksFailedToComplete); - return $"Failed to complete: {failedString}"; - } - } - public bool DoneCommand() { CPH.TryGetArg("rawInput", out string rawInput); - List userTasks = ListUserTasks(); + var userTasks = operations.ListUserTasks(); if (userTasks.Count == 0) { Respond("Error 404: Tasks not found"); return false; } - List tasksToBeCompleted = ParseTasksInput(rawInput); + var tasksToBeCompleted = TaskHelpers.ParseTasksInput(rawInput, operations.GetFocusedTask); if (tasksToBeCompleted.Count == 0) { Respond("Error: Invalid input"); return false; } - List tasksCompleted = new List(); - List tasksFailedToComplete = new List(); - List taskIndices = new List(); - // Gather indices of tasks to be removed + var tasksCompleted = new List(); + var tasksFailedToComplete = new List(); + var taskIndices = new List(); + foreach (string task in tasksToBeCompleted) { - int index = GetTaskIndex(userTasks, task); + int index = TaskHelpers.GetTaskIndex(userTasks, task); if (index > -1 && !taskIndices.Contains(index) && !userTasks[index].Completed) { taskIndices.Add(index); - tasksCompleted.Add(userTasks[index].Name); // Track what we're removing + tasksCompleted.Add(userTasks[index].Name); } else { @@ -1008,77 +889,62 @@ public class CPHInline } } - // Nothing valid to complete if (taskIndices.Count == 0) { - string failedString = String.Join(", ", tasksFailedToComplete); - Respond($"Failed to complete task(s): {failedString}"); + Respond($"Failed to complete task(s): {String.Join(", ", tasksFailedToComplete)}"); return false; } - // Mark all as complete foreach (int i in taskIndices) { userTasks[i].Completed = true; userTasks[i].Focused = false; - Broadcast(new { mode = "done", index = i }); + Broadcast(new { mode = "done", index = i }, null); } - // Save data - SaveIntoTasks(userTasks); - Respond(BuildCompletedMessage(tasksCompleted, tasksFailedToComplete)); + operations.SaveIntoTasks(userTasks); + SaveTasks(); + Respond(MessageBuilder.BuildCompletedMessage(tasksCompleted, tasksFailedToComplete)); return true; } - // unfocus - private void Unfocus() - { - List tasks = ListUserTasks(); - for (int i = 0; i < tasks.Count; i++) - { - tasks[i].Focused = false; - } - - SaveIntoTasks(tasks); - } - public bool UnfocusCommand() { - Unfocus(); + operations.Unfocus(); + SaveTasks(); Respond("Task have been unfocused!"); - Broadcast(new { mode = "unfocus" }); + Broadcast(new { mode = "unfocus" }, null); return true; } - // undone public bool UndoneCommand() { CPH.TryGetArg("rawInput", out string rawInput); - List userTasks = ListUserTasks(); + var userTasks = operations.ListUserTasks(); if (userTasks.Count == 0) { Respond("Error 404: Tasks not found"); return false; } - List tasksToBeCompleted = ParseUndoneInput(rawInput); + var tasksToBeCompleted = TaskHelpers.ParseUndoneInput(rawInput, userTasks); if (tasksToBeCompleted.Count == 0) { Respond("Error: Invalid input"); return false; } - List tasksCompleted = new List(); - List tasksFailedToComplete = new List(); - List taskIndices = new List(); - // Gather indices of tasks to be removed + var tasksCompleted = new List(); + var tasksFailedToComplete = new List(); + var taskIndices = new List(); + foreach (string task in tasksToBeCompleted) { - int index = GetTaskIndex(userTasks, task); + int index = TaskHelpers.GetTaskIndex(userTasks, task); if (index > -1 && !taskIndices.Contains(index) && userTasks[index].Completed) { taskIndices.Add(index); - tasksCompleted.Add(userTasks[index].Name); // Track what we're removing + tasksCompleted.Add(userTasks[index].Name); } else { @@ -1086,137 +952,67 @@ public class CPHInline } } - // Nothing valid to complete if (taskIndices.Count == 0) { - string failedString = String.Join(", ", tasksFailedToComplete); - Respond($"Failed to un-done task(s): {failedString}"); + Respond($"Failed to un-done task(s): {String.Join(", ", tasksFailedToComplete)}"); return false; } - // Mark all as complete foreach (int i in taskIndices) { userTasks[i].Completed = false; userTasks[i].Focused = false; - Broadcast(new { mode = "undone", index = i }); + Broadcast(new { mode = "undone", index = i }, null); } - // Save data - SaveIntoTasks(userTasks); - Respond(BuildUndoneMessage(tasksCompleted, tasksFailedToComplete)); + operations.SaveIntoTasks(userTasks); + SaveTasks(); + Respond(MessageBuilder.BuildUndoneMessage(tasksCompleted, tasksFailedToComplete)); return true; } - private List ParseUndoneInput(string input) - { - input = input.Trim(); - var tasksToRemove = new List(); - bool IsSpaceSeparatedInts = Regex.IsMatch(input, @"^\d+(\s+\d+)*$"); - if (IsSpaceSeparatedInts) - { - return input.Split(' ').ToList(); - } - - var splittedInput = input.Split(separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); - if (splittedInput.Count == 0) - { - // get the only completed task - List userTasks = ListUserTasks(); - int soloCompletedTaskIndex = -1; - var completedTasks = userTasks.Where(t => t.Completed); - if (completedTasks.Count() == 1) - { - soloCompletedTaskIndex = userTasks.FindIndex(t => t.Completed) + 1; - return new List - { - soloCompletedTaskIndex.ToString() - }; - } - } - else - { - return splittedInput; - } - - return new() - { - }; - } - - private string BuildUndoneMessage(List tasksCompleted, List tasksFailedToComplete) - { - // Build response - // string completedString = String.Join(", ", tasksCompleted); - if (tasksFailedToComplete.Count == 0 && tasksCompleted.Count == 1) - { - return "Task marked as incomplete!"; - } - else if (tasksFailedToComplete.Count == 0) - { - return "Task(s) marked as incomplete!"; - } - else - { - string failedString = String.Join(", ", tasksFailedToComplete); - return $"Failed to parse: {failedString}"; - } - } - - // CLEAR TASKS public bool ClearAllCommand() { - // clear all TASKS only - foreach (var item in this.taskData) - { - this.taskData[item.Key].tasks = new() - { - }; - } - - Cleanup(false); + operations.ClearAllTasks(); + operations.Cleanup(false); SaveTasks(); - Broadcast(new { mode = "clearall" }); + Broadcast(new { mode = "clearall" }, null); Respond("All tasks have been cleared!"); return true; } public bool ClearMyDoneCommand() { - // clear all of the user's completed tasks only string key = GetKey(); - this.taskData[key].tasks = this.taskData[key].tasks.Where(t => !t.Completed).ToList(); - Cleanup(false); + operations.ClearUserCompletedTasks(key); + operations.Cleanup(false); SaveTasks(); - Broadcast(new { mode = "clearmydone" }); + Broadcast(new { mode = "clearmydone" }, null); Respond("All of your completed tasks have been cleared!"); return true; } public bool ClearDoneCommand() { - // clear all COMPLETED TASKS only - foreach (var item in this.taskData) - { - this.taskData[item.Key].tasks = this.taskData[item.Key].tasks.Where(t => !t.Completed).ToList(); - } - - Cleanup(false); + operations.ClearCompletedTasks(); + operations.Cleanup(false); SaveTasks(); - Broadcast(new { mode = "cleardone" }); + Broadcast(new { mode = "cleardone" }, null); Respond("All completed tasks have been cleared!"); return true; } public bool ClearNotStreamerCommand() { - var streamersUsernames = GetStreamerUsernames(); - // clear all TASKS only if not - this.taskData = this.taskData.Where(kvp => streamersUsernames.Any(s => string.Equals(s, kvp.Value.username, StringComparison.OrdinalIgnoreCase))).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - Cleanup(false); + operations.FilterToStreamers(GetStreamerUsernames()); + operations.Cleanup(false); SaveTasks(); - Broadcast(new { mode = "clearns" }); + Broadcast(new { mode = "clearns" }, null); Respond("All tasks (excluding the streamer's) have been cleared!"); return true; } -} \ No newline at end of file + + #endregion +} + +#endregion \ No newline at end of file