using System; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using S22.Xmpp; using S22.Xmpp.Client; using S22.Xmpp.Im; using SBRL.Geometry; namespace RhinoReminds { public class ClientListener { public bool Debug { get; set; } = false; public readonly string Jid; public string Username => Jid.Split('@')[0]; public string Hostname => Jid.Split('@')[1]; private readonly string password; public string ReminderFilePath { get; set; } = "./Reminders.json"; private ReminderList reminderList = new ReminderList(); private XmppClient client; public ClientListener(string inJid, string inPassword) { Jid = inJid; password = inPassword; } public async Task Start() { if (File.Exists(ReminderFilePath)) { Console.WriteLine($"[Rhino/Startup] Loading reminders list from {ReminderFilePath}"); reminderList = JsonConvert.DeserializeObject(File.ReadAllText(ReminderFilePath)); } client = new XmppClient(Hostname, Username, password); client.Error += errorHandler; client.Message += messageHandlerRoot; client.SubscriptionRequest += subscriptionRequestHandler; // Connect to the server. This starts it's own thread that doesn't block the program exiting, apparently client.Connect(); while (!client.Connected) await Task.Delay(100); Console.WriteLine($"[Rhino/Setup] Connected as {Jid}"); //client.SetStatus(Availability.Online); await watchForReminders(); } #region XMPP Event Handling private bool subscriptionRequestHandler(Jid from) { Console.WriteLine($"[Rhino/SubscriptionRequest] Approving subscription from {from}"); return true; } private void errorHandler(object sender, S22.Xmpp.Im.ErrorEventArgs e) { Console.Error.WriteLine($"Error {e.Reason}: {e.Exception}"); } private void messageHandlerRoot(object sender, MessageEventArgs eventArgs) { Console.WriteLine($"[Rhino/Reciever] [Message] {eventArgs.Jid} | {eventArgs.Message.Body}"); try { messageHandler(eventArgs.Message); } catch (Exception error) { Console.Error.WriteLine(error); sendChatReply(eventArgs.Message, "Oops! I encountered an error. Please report this to my operator!"); sendChatReply(eventArgs.Message, $"Technical details: {WebUtility.HtmlEncode(error.ToString())} (stack trace available in server log) "); } } private void messageHandler(Message message) { string messageText = message.Body; string[] parts = Regex.Split(messageText.Trim(), @"\s+"); string instruction = parts[0].ToLower(); switch (parts[0].ToLower()) { case "list": case "show": if (parts.Select((n) => n.ToLower()).Contains("all")) { // TODO: Make sure that you can't see other people's reminders StringBuilder listMessage = new StringBuilder("I've got the following reminders on my list:\n"); foreach (Reminder nextReminder in reminderList.Reminders.Values) { listMessage.AppendLine($" - {nextReminder.Message} at {nextReminder.Time}"); } listMessage.AppendLine(); listMessage.AppendLine($"({reminderList.Reminders.Count} total)"); sendChatReply(message, listMessage.ToString()); return; } sendChatReply(message, "Sorry, I can't show individual items on my list right now. Try saying 'list all' to see all of them!"); // TODO: Identify number break; case "remind": Console.WriteLine("[Rhino/Reciever] Identified remind request"); DateTime dateTime; string rawDateTimeString; try { dateTime = AIRecogniser.RecogniseDateTime(messageText, out rawDateTimeString); } catch (AIException error) { sendChatReply(message, "Sorry, I had trouble figuring out when you wanted reminding about that!"); sendChatReply(message, $"(Technical details: {error.Message})"); return; } Range dateStringLocation = new Range( messageText.IndexOf(rawDateTimeString, StringComparison.OrdinalIgnoreCase), messageText.IndexOf(rawDateTimeString, StringComparison.OrdinalIgnoreCase) + rawDateTimeString.Length ); string reminder = Regex.Replace( messageText.Remove(dateStringLocation.Min, dateStringLocation.Stride), @"^remind\s+(?:me\s+)?", "", RegexOptions.IgnoreCase ).Replace(@"\s{2,}", " ").Trim(); if (Debug) { sendChatReply(message, $"[debug] Raw date identified: [{rawDateTimeString}]"); sendChatReply(message, $"[debug] Time identified at {dateStringLocation}"); sendChatReply(message, $"[debug] Transforming message - phase #1 [{reminder}]"); sendChatReply(message, $"[debug] Transforming message - phase #2 [{reminder}]"); } sendChatReply(message, $"Ok! I'll remind you {reminder} at {dateTime}."); Reminder newReminder = reminderList.CreateReminder(message.From, dateTime, reminder); reminderList.Save(ReminderFilePath); break; default: sendChatReply(message, "I don't understand that. Try rephrasing it or asking for help."); break; } } private void sendChatMessage(Jid to, string message) { Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}"); client.SendMessage( to, message, null, null, MessageType.Chat ); } private void sendChatReply(Message originalMessage, string reply) { Console.WriteLine($"[Rhino/Send/Reply] Sending {reply} -> {originalMessage.From}"); client.SendMessage( originalMessage.From, reply, null, originalMessage.Thread, MessageType.Chat ); } #endregion #region Reminder Listening private async Task watchForReminders() { CancellationTokenSource cancellationSource = new CancellationTokenSource(); CancellationToken cancellationToken = cancellationSource.Token; Reminder nextReminder = reminderList.GetNextReminder(); reminderList.OnReminderListUpdate += (object sender, Reminder newReminder) => { Reminder newNextReminder = reminderList.GetNextReminder(); Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing."); Console.WriteLine($"[Rhino/Reminderd/Canceller] {nextReminder} / {newNextReminder}"); if (nextReminder != newNextReminder) { Console.WriteLine($"[Rhino/Reminderd/Canceller] Cancelling"); nextReminder = newNextReminder; cancellationSource.Cancel(); } }; while (true) { TimeSpan nextWaitingTime; try { if (nextReminder != null) { nextWaitingTime = nextReminder.Time - DateTime.Now; if (DateTime.Now < nextReminder.Time) { Console.WriteLine($"[Rhino/Reminderd] Sleeping for {nextWaitingTime}"); await Task.Delay(nextWaitingTime, cancellationToken); } } else { Console.WriteLine("[Rhino/Reminderd] Sleeping until interrupted"); await Task.Delay(Timeout.Infinite, cancellationToken); } } catch (TaskCanceledException) { Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating"); cancellationSource = new CancellationTokenSource(); cancellationToken = cancellationSource.Token; continue; } if (cancellationToken.IsCancellationRequested) { Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating (but no exception thrown)"); cancellationSource = new CancellationTokenSource(); cancellationToken = cancellationSource.Token; continue; } Console.WriteLine($"[Rhino/Reminderd] Sending notification {nextReminder}"); sendChatMessage( nextReminder.Jid, $"Hello! You asked me to remind you {nextReminder.Message} at {nextReminder.Time}.".Replace(@"\s{2,}", " ").Trim() ); if (nextWaitingTime.TotalMilliseconds < 0) { sendChatMessage( nextReminder.Jid, "(Sorry I'm late reminding you! I might not have been running at the time, " + "or you might have scheduled a reminder for the past)" ); } reminderList.DeleteReminder(nextReminder); reminderList.Save(ReminderFilePath); nextReminder = reminderList.GetNextReminder(); } } #endregion } }