// Author: rythondev , https://twitch.tv/rythondev , https://x.com/rythondev, https://ko-fi.com/rython // Contact: rythondev@gmail.com , or on the above mentioned social media. // // This code is licensed under the GNU General Public License Version 3 (GPLv3). // // The GPLv3 is a free software license that ensures end users have the freedom to run, // study, share, and modify the software. Key provisions include: // // - Copyleft: Modified versions of the code must also be licensed under the GPLv3. // - Source Code: You must provide access to the source code when distributing the software. // - Credit: You must credit the original author of the software, by mentioning either contact e-mail or their social media. // - No Warranty: The software is provided "as-is," without warranty of any kind. // // For more details, see https://www.gnu.org/licenses/gpl-3.0.en.html. using Streamer.bot.Plugin.Interface; using Streamer.bot.Plugin.Interface.Model; using Streamer.bot.Plugin.Interface.Enums; using Streamer.bot.Common.Events; using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Linq; using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; #region Data Models public class Task { public string Name; public bool Completed; public bool Focused; public DateTime AddedTime; public DateTime UpdatedTime; public DateTime? CompletedTime; public Task(string Name, bool Completed = false, bool Focused = false) { this.Name = Name; this.Completed = Completed; this.Focused = Focused; this.AddedTime = DateTime.Now; this.UpdatedTime = DateTime.Now; } } public class UserData { public string Username; public List Tasks; public int TotalCompletedCount; public UserData(List tasks, string username) { this.Username = username; this.Tasks = tasks; this.TotalCompletedCount = 0; } } public class Response { public bool Success { get; set; } public string ErrorMsg { get; set; } public Response(bool Success, string ErrorMsg = null) { this.Success = Success; this.ErrorMsg = ErrorMsg; } } public class Response : Response { public T Data { get; set; } public Response(bool success, T data = default(T), string errorMsg = null) : base(success, errorMsg) { this.Data = data; } } #endregion #region Helper Classes public static class TaskHelpers { public static readonly char[] Separators = { '|', ',', ';' }; public const int CharacterLimit = 450; public static List SplitTasks(string tasks) { return tasks.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim()).ToList(); } // return true index public static int GetTaskIndex(List tasks, string input) { 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(); if (input == "all") { return new() { "all" }; } 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, BotResponses.EditNotEnoughArguments); } } 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, BotResponses.EditInvalidArgument); } } if (validEditInput) { return new Response<(int, string)>(true, (index - 1, newTask), null); } return new Response<(int, string)>(true, (focusedTaskIndex, rawInput), null); } } #region Response Text public static class BotResponses { // Customize chat-facing text in this section when translating the bot. public const string Help = "📝 Rython Task Bot Commands: !task !edit !remove !done. For mods, you can do !adel @user. More commmands here: https://github.com/liyunze-coding/rython-task-bot-v2#usage"; public const string EditNotEnoughArguments = "❌ Error: Not enough arguments, try !edit "; public const string EditInvalidArgument = "❌ Error: Not a valid argument, try !edit "; public const string TaskCannotBeEmpty = "❌ Error: Task cannot be empty"; public const string TaskCannotBeNumber = "❌ Error: Task cannot be a number"; public const string ReservedAllKeyword = "❌ Error: 'All' is a reserved keyword"; public const string TasksNotFound = "❌ Error 404: Tasks not found"; public const string TasksNotFoundShort = "❌ 404 Tasks not found"; public const string InvalidTaskNumber = "❌ Error: Invalid task number"; public const string TaskAlreadyExists = "❌ Error: Task already exists"; public const string CannotFocusMultipleTasks = "❌ Cannot focus on multiple tasks"; public const string IndexOutOfRange = "❌ Error: Index out of range."; public const string CannotFocusCompletedTask = "❌ Error: Cannot focus on completed task."; public const string CannotFocusCompletedTaskShort = "❌ Cannot focus on completed task."; public const string NoTasksProvided = "❌ No tasks provided"; public const string NoTasksProvidedWithError = "❌ Error: No tasks provided"; public const string NoFocusedTask = "❌ You do not have a focused task."; public const string NextNeedsFocusedTask = "❌ Can't use !next command, select a task to complete using !done and/or add another task"; public const string UserNotFound = "❌ User not found"; public const string InvalidInput = "❌ Error: Invalid input"; public const string AdminDeleteUserNotFound = "❌ Error: Unable to find user with that username on the task list"; public const string AdminDeleteSuccess = "All of the user's tasks have been deleted"; public const string Unfocused = "📝 Task have been unfocused!"; public const string ClearAll = "📝 All tasks have been cleared!"; public const string ClearMyDone = "📝 All of your completed tasks have been cleared!"; public const string ClearDone = "📝 All completed tasks have been cleared!"; public const string ClearNotStreamer = "📝 All tasks (excluding the streamer's) have been cleared!"; public const string InvalidDaysArgument = "❌ Error: Please provide a valid number of days, e.g. !clearold 7"; public const string AddSuccessShort = "Task List Update ♡ → Your task(s) have been added! Good luck! 🍀"; public const string AddFailedShort = "❌ Failed to add your task(s)"; public const string AddPartialShort = "Task List Update ♡ → some tasks were successful but some failed :p"; public const string NoTasksToAdd = "No tasks to add"; public const string LogSuccessShort = "Task List Update ♡ → Your task(s) have been logged! Good job! 🍀"; public const string LogFailedShort = "❌ Failed to logged your task(s)"; public const string LogPartialShort = "Task List Update ♡ → some tasks were logged successfully but some failed :p"; public const string NoTasksToLog = "No tasks to log"; public const string RemoveAllSuccess = "Tasklist Update 🚮 → All your tasks have been removed!"; public const string RemoveMultipleSuccess = "Tasklist Update 🚮 → Tasks removed successfully!"; public const string CompleteAllSuccess = "Task list updated! 🎉 Good job on completing all your tasks!"; public const string CompleteMultipleSuccess = "Task list updated! 🎉 Good job on finishing your tasks!"; public const string UndoneMultipleSuccess = "Task list updated 📝 — Tasks marked as incomplete"; public const string FocusedMarker = "(ongoing) "; public static string TaskAlreadyExistsWithName(string taskName) => $"❌ Error: Task '{taskName}' already exists"; public static string AddSuccess(string addedTasks) => $"Task List Update ♡ → The task(s): \"{addedTasks}\" have been added! Good luck! 🍀"; public static string AddFailed(string failedTasks) => $"❌ Failed to add your task(s) {failedTasks}"; public static string AddPartial(string addedTasks, string failedTasks) => $"Task List Update ♡ → Task(s) added: \"{addedTasks}\" | Failed: {failedTasks}"; public static string LogSuccess(string loggedTasks) => $"Task List Update ♡ → The task(s) {loggedTasks} have been logged! Good job! 🍀"; public static string LogFailed(string failedTasks) => $"❌ Failed to logged your task(s) {failedTasks}"; public static string LogPartial(string loggedTasks, string failedTasks) => $"Task List Update ♡ → Task(s) logged: {loggedTasks} | Failed: {failedTasks}"; public static string RemoveSingleSuccess(string taskName) => $"Tasklist Update 🚮 → Task '{taskName}' has been deleted successfully!"; public static string RemoveFailed(string failedTasks) => $"Failed to remove: {failedTasks}"; public static string CompleteSingleSuccess(string taskName) => $"Task list updated! 🎉 Good job on finishing the task '{taskName}'!"; public static string CompleteFailed(string failedTasks) => $"❌ Failed to complete: {failedTasks}"; public static string UndoneSingleSuccess(string taskName) => $"Task list updated 📝 — Task {taskName} marked as incomplete!"; public static string UndoneFailed(string failedTasks) => $"❌ Failed to mark as incomplete: {failedTasks}"; public static string FocusedTask(int taskNumber, string taskName) => $"✅ Current focused task: {taskNumber}. {taskName}"; public static string NextSuccess(string completedTaskName, int nextTaskNumber, string nextTaskName) => $"✅ Good job on completing '{completedTaskName}'! Moving onto '({nextTaskNumber}) {nextTaskName}'"; public static string EditSuccess(string oldTaskName, string newTaskName) => $"📝 Task '{oldTaskName}' has been edited to '{newTaskName}'"; public static string PendingTaskItem(int taskNumber, string taskName, bool focused) => $"{taskNumber}. {(focused ? FocusedMarker : "")}{taskName}"; public static string PendingTasks(int count, string tasks) => $"Task(s) pending ({count}): {tasks}"; public static string OtherUserPendingTasks(string username, int count, string tasks) => $"{username}'s task(s) ({count}): {tasks}"; public static string CompletedTaskItem(int taskNumber, string taskName) => $"{taskNumber}. {taskName}"; public static string CompletedTasks(int count, string tasks) => $"Completed {count} tasks: {tasks}"; public static string OtherUserCompletedTasks(string username, int count, string tasks) => $"{username}'s tasks ({count}): {tasks}"; public static string FailedToRemoveTasks(string failedTasks) => $"❌ Failed to remove task(s): {failedTasks}"; public static string FailedToCompleteTasks(string failedTasks) => $"❌ Failed to complete task(s): {failedTasks}"; public static string FailedToUndoneTasks(string failedTasks) => $"❌ Failed to un-done task(s): {failedTasks}"; public static string OtherUserCount(string username, int completedCount) => $"{username} has completed {completedCount} task(s) so far!"; public static string MyCount(int completedCount) => $"You have completed {completedCount} task(s) so far!"; public static string ClearOld(int count, int days) => $"📝 Cleared {count} task(s) that were {days} day(s) or older!"; public static string CountOld(int count, int days) => $"📝 There are {count} task(s) that are {days} day(s) or older."; } #endregion public static class MessageBuilder { public static string BuildAddResponseMessage(List added, List<(string, string)> failed) { if (added.Count > 0 && failed.Count == 0) { string response = BotResponses.AddSuccess(String.Join("\", \"", added)); return response.Length > TaskHelpers.CharacterLimit ? BotResponses.AddSuccessShort : response; } if (added.Count == 0 && failed.Count == 1) { return failed[0].Item2; } if (added.Count == 0 && failed.Count > 1) { string response = BotResponses.AddFailed(String.Join(", ", failed.Select(f => f.Item1))); return response.Length > TaskHelpers.CharacterLimit ? BotResponses.AddFailedShort : response; } if (added.Count > 0 && failed.Count > 0) { string response = BotResponses.AddPartial(String.Join("\", \"", added), String.Join(", ", failed.Select(f => f.Item1))); return response.Length > TaskHelpers.CharacterLimit ? BotResponses.AddPartialShort : response; } return BotResponses.NoTasksToAdd; } public static string BuildLogResponseMessage(List logged, List<(string, string)> failed) { if (logged.Count > 0 && failed.Count == 0) { string response = BotResponses.LogSuccess(String.Join(", ", logged)); return response.Length > TaskHelpers.CharacterLimit ? BotResponses.LogSuccessShort : response; } if (logged.Count == 0 && failed.Count == 1) { return failed[0].Item2; } if (logged.Count == 0 && failed.Count > 1) { string response = BotResponses.LogFailed(String.Join(", ", failed.Select(f => f.Item1))); return response.Length > TaskHelpers.CharacterLimit ? BotResponses.LogFailedShort : response; } if (logged.Count > 0 && failed.Count > 0) { string response = BotResponses.LogPartial(String.Join(" | ", logged), String.Join(", ", failed.Select(f => f.Item1))); return response.Length > TaskHelpers.CharacterLimit ? BotResponses.LogPartialShort : response; } return BotResponses.NoTasksToLog; } public static string BuildRemoveMessage(List tasksRemoved, List tasksFailedToRemove, bool allTasks) { if (allTasks) { return BotResponses.RemoveAllSuccess; } if (tasksFailedToRemove.Count == 0 && tasksRemoved.Count == 1) { return BotResponses.RemoveSingleSuccess(tasksRemoved[0]); } if (tasksFailedToRemove.Count == 0) { return BotResponses.RemoveMultipleSuccess; } return BotResponses.RemoveFailed(String.Join(", ", tasksFailedToRemove)); } public static string BuildCompletedMessage(List tasksCompleted, List tasksFailedToComplete, bool allTasks) { if (allTasks) { return BotResponses.CompleteAllSuccess; } if (tasksFailedToComplete.Count == 0 && tasksCompleted.Count == 1) { return BotResponses.CompleteSingleSuccess(tasksCompleted[0]); } if (tasksFailedToComplete.Count == 0) { return BotResponses.CompleteMultipleSuccess; } return BotResponses.CompleteFailed(String.Join(", ", tasksFailedToComplete)); } public static string BuildUndoneMessage(List tasksIncomplete, List tasksFailedToComplete) { if (tasksFailedToComplete.Count == 0 && tasksIncomplete.Count == 1) { return BotResponses.UndoneSingleSuccess(tasksIncomplete[0]); } if (tasksFailedToComplete.Count == 0) { return BotResponses.UndoneMultipleSuccess; } return BotResponses.UndoneFailed(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, BotResponses.TaskCannotBeEmpty); if (int.TryParse(taskName, out _)) return new Response<(int, string)>(false, default, BotResponses.TaskCannotBeNumber); if (taskName.Equals("all", StringComparison.OrdinalIgnoreCase)) return new Response<(int, string)>(false, default, BotResponses.ReservedAllKeyword); 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, BotResponses.TaskAlreadyExistsWithName(taskName)); if (focused) Unfocus(); taskData[key].Username = getUsername(); taskData[key].Tasks.Add(new Task(taskName, completed, focused)); if (completed) { taskData[key].TotalCompletedCount++; } 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, BotResponses.TasksNotFound); if (index >= userTasks.Count || index < 0) return new Response<(string, string)>(false, default, BotResponses.InvalidTaskNumber); bool taskAlreadyExists = userTasks.FindIndex(t => t.Name.Equals(newTask, StringComparison.OrdinalIgnoreCase)) > -1; if (taskAlreadyExists) return new Response<(string, string)>(false, default, BotResponses.TaskAlreadyExists); 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, BotResponses.CannotFocusMultipleTasks); 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, BotResponses.IndexOutOfRange); if (tasks[n].Completed) return new Response<(int, string)>(false, default, BotResponses.CannotFocusCompletedTask); 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, BotResponses.CannotFocusCompletedTaskShort); 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(); tasks = UnfocusAll(tasks); SaveIntoTasks(tasks); } private List UnfocusAll(List tasks) { for (int i = 0; i < tasks.Count; i++) tasks[i].Focused = false; return tasks; } 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 int CountOldTasks(int days) { DateTime cutoff = DateTime.Now.AddDays(-days); return taskData.Values.Sum(user => user.Tasks.Count(t => t.AddedTime <= cutoff)); } public int ClearOldTasks(int days) { DateTime cutoff = DateTime.Now.AddDays(-days); int removed = 0; foreach (var key in taskData.Keys.ToList()) { int before = taskData[key].Tasks.Count; taskData[key].Tasks = taskData[key].Tasks.Where(t => t.AddedTime > cutoff).ToList(); removed += before - taskData[key].Tasks.Count; } return removed; } 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); 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); } private List GetStreamerUsernames() { TwitchUserInfo twitchInfo = CPH.TwitchGetBroadcaster(); var youtubeInfo = CPH.YouTubeGetBroadcaster(); return new List { twitchInfo.UserName, youtubeInfo.UserName }; } private string GetUsername(string key = null) { if (String.IsNullOrEmpty(key)) { CPH.TryGetArg("user", out string username); return username; } return taskData[key].Username; } private string GetKey(string username = null) { if (!String.IsNullOrEmpty(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 ""; } CPH.TryGetArg("userType", out string platform); CPH.TryGetArg("userId", out string userId); return $"{platform}-{userId}"; } private void IncrementDoneCount(int count = 1) { string key = GetKey(); taskData[key].TotalCompletedCount += count; } private void SaveTasks() { taskData = operations.GetTaskData(); string taskDataString = JsonConvert.SerializeObject(taskData); CPH.SetGlobalVar("rython-task-bot", taskDataString, true); } private int GetCount(string? key = null) { if (String.IsNullOrEmpty(key)) { key = GetKey(); } return taskData[key].TotalCompletedCount; } private void Respond(string message) { int maxChars = 500; CPH.TryGetArg("userType", out string platform); if (platform == "youtube") { maxChars = 200; } string[] words = message.Split(' '); string output = ""; foreach (string word in words) { if ((output + " " + word).Length > maxChars) { SendMessage(output.Trim(), platform); output = word; CPH.Wait(100); } else { output += (string.IsNullOrEmpty(output) ? "" : " ") + word; } } if (!string.IsNullOrEmpty(output)) { SendMessage(output.Trim(), platform); } } private void SendMessage(string message, string platform = "twitch") { switch (platform) { case "twitch": CPH.TryGetArg("msgId", out string twitchMsgId); CPH.TwitchReplyToMessage(message, twitchMsgId); break; case "youtube": CPH.TryGetArg("user", out string YTUser); CPH.SendYouTubeMessage($"@{YTUser} {message}"); break; case "kick": CPH.TryGetArg("user", out string kickUser); CPH.TryGetArg("msgId", out string kickMsgId); CPH.KickReplyToMessage($"@{kickUser} {message}", kickMsgId); break; default: break; } } #endregion #region Commands public bool HelpCommand() { Respond(BotResponses.Help); return true; } public bool AddCommand() { CPH.TryGetArg("rawInput", out string rawInput); var taskStrings = TaskHelpers.SplitTasks(rawInput); if (taskStrings.Count == 0) { Respond(BotResponses.NoTasksProvided); return false; } var added = new List(); var failed = new List<(string, string)>(); foreach (string taskString in taskStrings) { var response = operations.AddTask(taskString); if (response.Success) added.Add($"{response.Data.Item1 + 1}. {taskString.Trim()}"); else failed.Add((taskString, response.ErrorMsg)); } if (added.Count > 0) SaveTasks(); Respond(MessageBuilder.BuildAddResponseMessage(added, failed)); return true; } public bool LogCommand() { CPH.TryGetArg("rawInput", out string rawInput); var taskStrings = TaskHelpers.SplitTasks(rawInput); if (taskStrings.Count == 0) { Respond(BotResponses.NoTasksProvidedWithError); return false; } var logged = new List(); var failed = new List<(string, string)>(); foreach (string taskString in taskStrings) { var response = operations.AddTask(taskString, true, false); if (response.Success) logged.Add($"{response.Data.Item1 + 1}. {taskString.Trim()}"); else failed.Add((taskString, response.ErrorMsg)); } if (logged.Count > 0) SaveTasks(); Respond(MessageBuilder.BuildLogResponseMessage(logged, failed)); return true; } public bool FocusCommand() { CPH.TryGetArg("rawInput", out string rawInput); var response = operations.FocusOnTask(rawInput); if (!response.Success) Respond(response.ErrorMsg); else Respond(BotResponses.FocusedTask(response.Data.Item1 + 1, response.Data.Item2)); SaveTasks(); return true; } public bool FocusedCommand() { var userTasks = operations.ListUserTasks(); int focusedTaskIndex = operations.GetFocusedTask(); if (focusedTaskIndex == -1) { Respond(BotResponses.NoFocusedTask); return true; } Respond(BotResponses.FocusedTask(focusedTaskIndex + 1, userTasks[focusedTaskIndex].Name)); return true; } public bool NextCommand() { var userTasks = operations.ListUserTasks(); int focusedTaskIndex = operations.GetFocusedTask(); if (focusedTaskIndex == -1) { Respond(BotResponses.NextNeedsFocusedTask); return false; } CPH.TryGetArg("rawInput", out string rawInput); var focusResponse = operations.FocusOnTask(rawInput); if (!focusResponse.Success) { Respond(focusResponse.ErrorMsg); return false; } string completedTaskName = userTasks[focusedTaskIndex].Name; userTasks[focusedTaskIndex].Completed = true; userTasks[focusedTaskIndex].Focused = false; operations.SaveIntoTasks(userTasks); SaveTasks(); Broadcast(new { mode = "done", index = focusedTaskIndex }, null); Respond(BotResponses.NextSuccess(completedTaskName, focusResponse.Data.Item1 + 1, focusResponse.Data.Item2)); return true; } public bool EditCommand() { CPH.TryGetArg("rawInput", out string rawInput); var editData = TaskHelpers.ParseEditInput(rawInput, operations.GetFocusedTask()); if (!editData.Success) { Respond(editData.ErrorMsg); return false; } var result = operations.EditTask(editData.Data.Item1, editData.Data.Item2); Respond(result.Success ? BotResponses.EditSuccess(result.Data.Item1, result.Data.Item2) : result.ErrorMsg); SaveTasks(); return true; } public bool CheckCommand() { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); string key = null; bool someoneElse = false; if (!String.IsNullOrWhiteSpace(rawInput)) { key = GetKey(rawInput); if (key == "") { Respond(BotResponses.UserNotFound); return false; } someoneElse = true; } var userTasks = operations.ListUserTasks(key); if (userTasks.Count == 0) { Respond(BotResponses.TasksNotFoundShort); return false; } var incompleteTasks = userTasks.Select((t, index) => new { t, index }).Where(x => !x.t.Completed); string message = String.Join(" | ", incompleteTasks.Select(x => BotResponses.PendingTaskItem(x.index + 1, x.t.Name, x.t.Focused))); message = someoneElse ? BotResponses.OtherUserPendingTasks(GetUsername(key), incompleteTasks.Count(), message) : BotResponses.PendingTasks(incompleteTasks.Count(), message); Respond(message); return true; } public bool CompletedCommand() { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); string key = null; bool someoneElse = false; if (!String.IsNullOrWhiteSpace(rawInput)) { key = GetKey(rawInput); if (key == "") { Respond(BotResponses.UserNotFound); return false; } someoneElse = true; } var userTasks = operations.ListUserTasks(key); if (userTasks.Count == 0) { Respond(BotResponses.TasksNotFoundShort); return false; } var completedTasks = userTasks.Select((t, index) => new { t, index }).Where(x => x.t.Completed); string message = String.Join(" | ", completedTasks.Select(x => BotResponses.CompletedTaskItem(x.index + 1, x.t.Name))); message = someoneElse ? BotResponses.OtherUserCompletedTasks(GetUsername(key), completedTasks.Count(), message) : BotResponses.CompletedTasks(completedTasks.Count(), message); Respond(message); return true; } public bool ListCommand() { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); string separator = "; "; if (rawInput.Length == 1 && ";,|".Contains(rawInput)) separator = $"{rawInput} "; var userTasks = operations.ListUserTasks(); string message = String.Join(separator, userTasks.Where(t => !t.Completed).Select(t => t.Name)); Respond(message); return true; } public bool RemoveCommand() { CPH.TryGetArg("rawInput", out string rawInput); var userTasks = operations.ListUserTasks(); if (userTasks.Count == 0) { Respond(BotResponses.TasksNotFound); return false; } var tasksToBeRemoved = TaskHelpers.ParseTasksInput(rawInput, operations.GetFocusedTask); if (tasksToBeRemoved.Count == 0) { Respond(BotResponses.InvalidInput); return false; } bool allTasks = tasksToBeRemoved.Count == 1 && tasksToBeRemoved[0] == "all"; var tasksRemoved = new List(); var tasksFailedToRemove = new List(); var taskIndices = new List(); if (!allTasks) { foreach (string task in tasksToBeRemoved) { int index = TaskHelpers.GetTaskIndex(userTasks, task); if (index > -1 && !taskIndices.Contains(index)) { taskIndices.Add(index); tasksRemoved.Add(userTasks[index].Name); } else { tasksFailedToRemove.Add(task); } } if (taskIndices.Count == 0) { Respond(BotResponses.FailedToRemoveTasks(String.Join(", ", tasksFailedToRemove))); return false; } } else { taskIndices = Enumerable.Range(0, userTasks.Count).ToList(); } foreach (int i in taskIndices.OrderByDescending(n => n)) { userTasks.RemoveAt(i); } operations.SaveIntoTasks(userTasks); operations.Cleanup(true); SaveTasks(); foreach (int i in taskIndices.OrderByDescending(n => n)) { Broadcast(new { mode = "remove", index = i }, null); } Respond(MessageBuilder.BuildRemoveMessage(tasksRemoved, tasksFailedToRemove, allTasks)); return true; } public bool AdminDelete() { CPH.TryGetArg("rawInput", out string rawInput); string[] spaceSeparated = rawInput.Split(new[] { ' ' }, 2); if (spaceSeparated.Length == 0) return false; string user = spaceSeparated[0]; string key = GetKey(user); if (key == "") { Respond(BotResponses.AdminDeleteUserNotFound); return false; } operations.RemoveUser(key); SaveTasks(); Respond(BotResponses.AdminDeleteSuccess); Broadcast(new { mode = "admindelete", id = key }, null); return true; } public bool DoneCommand() { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); var userTasks = operations.ListUserTasks(); if (userTasks.Count == 0) { Respond(BotResponses.TasksNotFound); return false; } var tasksToBeCompleted = TaskHelpers.ParseTasksInput(rawInput, operations.GetFocusedTask); if (tasksToBeCompleted.Count == 0) { Respond(BotResponses.InvalidInput); return false; } bool allTasks = tasksToBeCompleted.Count == 1 && tasksToBeCompleted[0] == "all"; var tasksCompleted = new List(); var tasksFailedToComplete = new List(); var taskIndices = new List(); if (!allTasks) { foreach (string task in tasksToBeCompleted) { int index = TaskHelpers.GetTaskIndex(userTasks, task); if (index > -1 && !taskIndices.Contains(index) && !userTasks[index].Completed) { taskIndices.Add(index); tasksCompleted.Add(userTasks[index].Name); } else { tasksFailedToComplete.Add(task); } } } else { taskIndices = Enumerable.Range(0, userTasks.Count).ToList(); } if (taskIndices.Count == 0) { Respond(BotResponses.FailedToCompleteTasks(String.Join(", ", tasksFailedToComplete))); return false; } foreach (int i in taskIndices) { userTasks[i].Completed = true; userTasks[i].Focused = false; } IncrementDoneCount(taskIndices.Count); operations.SaveIntoTasks(userTasks); SaveTasks(); foreach (int i in taskIndices) { Broadcast(new { mode = "done", index = i }, null); } Respond(MessageBuilder.BuildCompletedMessage(tasksCompleted, tasksFailedToComplete, allTasks)); return true; } public bool UnfocusCommand() { operations.Unfocus(); SaveTasks(); Respond(BotResponses.Unfocused); Broadcast(new { mode = "unfocus" }, null); return true; } public bool UndoneCommand() { CPH.TryGetArg("rawInput", out string rawInput); var userTasks = operations.ListUserTasks(); if (userTasks.Count == 0) { Respond(BotResponses.TasksNotFound); return false; } var tasksToBeCompleted = TaskHelpers.ParseUndoneInput(rawInput, userTasks); if (tasksToBeCompleted.Count == 0) { Respond(BotResponses.InvalidInput); return false; } var tasksCompleted = new List(); var tasksFailedToComplete = new List(); var taskIndices = new List(); foreach (string task in tasksToBeCompleted) { int index = TaskHelpers.GetTaskIndex(userTasks, task); if (index > -1 && !taskIndices.Contains(index) && userTasks[index].Completed) { taskIndices.Add(index); tasksCompleted.Add(userTasks[index].Name); } else { tasksFailedToComplete.Add(task); } } if (taskIndices.Count == 0) { Respond(BotResponses.FailedToUndoneTasks(String.Join(", ", tasksFailedToComplete))); return false; } foreach (int i in taskIndices) { userTasks[i].Completed = false; userTasks[i].Focused = false; } operations.SaveIntoTasks(userTasks); SaveTasks(); foreach (int i in taskIndices) { Broadcast(new { mode = "undone", index = i }, null); } Respond(MessageBuilder.BuildUndoneMessage(tasksCompleted, tasksFailedToComplete)); return true; } public bool ClearAllCommand() { operations.ClearAllTasks(); operations.Cleanup(false); SaveTasks(); Broadcast(new { mode = "clearall" }, null); Respond(BotResponses.ClearAll); return true; } public bool ClearMyDoneCommand() { string key = GetKey(); operations.ClearUserCompletedTasks(key); operations.Cleanup(false); SaveTasks(); Broadcast(new { mode = "clearmydone" }, null); Respond(BotResponses.ClearMyDone); return true; } public bool ClearDoneCommand() { operations.ClearCompletedTasks(); operations.Cleanup(false); SaveTasks(); Broadcast(new { mode = "cleardone" }, null); Respond(BotResponses.ClearDone); return true; } public bool ClearNotStreamerCommand() { operations.FilterToStreamers(GetStreamerUsernames()); operations.Cleanup(false); SaveTasks(); Broadcast(new { mode = "clearns" }, null); Respond(BotResponses.ClearNotStreamer); return true; } private bool TryParseDaysArgument(out int days) { days = 0; CPH.TryGetArg("rawInput", out string rawInput); if (string.IsNullOrWhiteSpace(rawInput) || !int.TryParse(rawInput.Trim(), out days) || days < 1) { Respond(BotResponses.InvalidDaysArgument); return false; } return true; } public bool ClearOldCommand() { if (!TryParseDaysArgument(out int days)) return false; int removed = operations.ClearOldTasks(days); operations.Cleanup(false); SaveTasks(); Broadcast(new { mode = "clearold" }, null); Respond(BotResponses.ClearOld(removed, days)); return true; } public bool CountOldCommand() { if (!TryParseDaysArgument(out int days)) return false; int count = operations.CountOldTasks(days); Respond(BotResponses.CountOld(count, days)); return true; } public bool CountCommand() { CPH.TryGetArg("rawInput", out string rawInput); rawInput = rawInput.Trim(); string key = null; bool someoneElse = false; if (!String.IsNullOrWhiteSpace(rawInput)) { key = GetKey(rawInput); if (key == "") { Respond(BotResponses.UserNotFound); return false; } someoneElse = true; } int completedCount = GetCount(key); if (someoneElse) { string username = GetUsername(key); Respond(BotResponses.OtherUserCount(username, completedCount)); } else { Respond(BotResponses.MyCount(completedCount)); } return true; } #endregion } #endregion