2018-11-10 01:19:54 +00:00
|
|
|
|
using System;
|
2018-11-10 18:15:30 +00:00
|
|
|
|
using System.IO;
|
2018-11-10 01:19:54 +00:00
|
|
|
|
using System.Linq;
|
2018-11-10 18:15:30 +00:00
|
|
|
|
using System.Net;
|
2018-11-10 18:31:58 +00:00
|
|
|
|
using System.Text;
|
2018-11-10 01:19:54 +00:00
|
|
|
|
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;
|
2018-11-10 18:15:30 +00:00
|
|
|
|
using SBRL.Geometry;
|
2018-11-10 01:19:54 +00:00
|
|
|
|
|
|
|
|
|
namespace RhinoReminds
|
|
|
|
|
{
|
|
|
|
|
public class ClientListener
|
|
|
|
|
{
|
2018-11-10 18:15:30 +00:00
|
|
|
|
public bool Debug { get; set; } = false;
|
|
|
|
|
|
2018-11-10 01:19:54 +00:00
|
|
|
|
public readonly string Jid;
|
|
|
|
|
public string Username => Jid.Split('@')[0];
|
|
|
|
|
public string Hostname => Jid.Split('@')[1];
|
|
|
|
|
private readonly string password;
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
public string ReminderFilePath { get; set; } = "./Reminders.json";
|
2018-11-10 18:31:58 +00:00
|
|
|
|
private ReminderList reminderList = new ReminderList();
|
2018-11-10 01:19:54 +00:00
|
|
|
|
|
|
|
|
|
private XmppClient client;
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
public ClientListener(string inJid, string inPassword)
|
|
|
|
|
{
|
2018-11-10 01:19:54 +00:00
|
|
|
|
Jid = inJid;
|
|
|
|
|
password = inPassword;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task Start()
|
|
|
|
|
{
|
2018-11-10 18:15:30 +00:00
|
|
|
|
if (File.Exists(ReminderFilePath))
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"[Rhino/Startup] Loading reminders list from {ReminderFilePath}");
|
2018-11-10 18:31:58 +00:00
|
|
|
|
reminderList = JsonConvert.DeserializeObject<ReminderList>(File.ReadAllText(ReminderFilePath));
|
2018-11-10 18:15:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 01:19:54 +00:00
|
|
|
|
client = new XmppClient(Hostname, Username, password);
|
|
|
|
|
client.Error += errorHandler;
|
2018-11-10 18:15:30 +00:00
|
|
|
|
client.Message += messageHandlerRoot;
|
2018-11-10 01:19:54 +00:00
|
|
|
|
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);
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
await watchForReminders();
|
2018-11-10 01:19:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
#region XMPP Event Handling
|
|
|
|
|
private bool subscriptionRequestHandler(Jid from)
|
|
|
|
|
{
|
2018-11-10 01:19:54 +00:00
|
|
|
|
Console.WriteLine($"[Rhino/SubscriptionRequest] Approving subscription from {from}");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
private void errorHandler(object sender, S22.Xmpp.Im.ErrorEventArgs e)
|
|
|
|
|
{
|
2018-11-10 01:19:54 +00:00
|
|
|
|
Console.Error.WriteLine($"Error {e.Reason}: {e.Exception}");
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
private void messageHandlerRoot(object sender, MessageEventArgs eventArgs)
|
|
|
|
|
{
|
2018-11-10 01:19:54 +00:00
|
|
|
|
Console.WriteLine($"[Rhino/Reciever] [Message] {eventArgs.Jid} | {eventArgs.Message.Body}");
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
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) ");
|
2018-11-10 01:19:54 +00:00
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-10 01:19:54 +00:00
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
private void messageHandler(Message message)
|
|
|
|
|
{
|
|
|
|
|
string messageText = message.Body;
|
|
|
|
|
string[] parts = Regex.Split(messageText.Trim(), @"\s+");
|
|
|
|
|
string instruction = parts[0].ToLower();
|
2018-11-10 01:19:54 +00:00
|
|
|
|
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
switch (parts[0].ToLower())
|
|
|
|
|
{
|
|
|
|
|
case "list":
|
|
|
|
|
case "show":
|
|
|
|
|
if (parts.Select((n) => n.ToLower()).Contains("all")) {
|
2018-11-10 18:31:58 +00:00
|
|
|
|
// 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;
|
2018-11-10 18:15:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:31:58 +00:00
|
|
|
|
sendChatReply(message, "Sorry, I can't show individual items on my list right now. Try saying 'list all' to see all of them!");
|
2018-11-10 18:15:30 +00:00
|
|
|
|
// 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}.");
|
|
|
|
|
|
2018-11-10 18:31:58 +00:00
|
|
|
|
Reminder newReminder = reminderList.CreateReminder(message.From, dateTime, reminder);
|
|
|
|
|
reminderList.Save(ReminderFilePath);
|
2018-11-10 18:15:30 +00:00
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
sendChatReply(message, "I don't understand that. Try rephrasing it or asking for help.");
|
|
|
|
|
break;
|
2018-11-10 01:19:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
private void sendChatMessage(Jid to, string message) {
|
|
|
|
|
Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}");
|
|
|
|
|
client.SendMessage(
|
|
|
|
|
to, message,
|
|
|
|
|
null, null, MessageType.Chat
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-11-10 01:19:54 +00:00
|
|
|
|
private void sendChatReply(Message originalMessage, string reply)
|
|
|
|
|
{
|
2018-11-10 18:15:30 +00:00
|
|
|
|
Console.WriteLine($"[Rhino/Send/Reply] Sending {reply} -> {originalMessage.From}");
|
2018-11-10 01:19:54 +00:00
|
|
|
|
client.SendMessage(
|
|
|
|
|
originalMessage.From, reply,
|
|
|
|
|
null, originalMessage.Thread, MessageType.Chat
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:15:30 +00:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Reminder Listening
|
|
|
|
|
|
|
|
|
|
private async Task watchForReminders()
|
2018-11-10 01:19:54 +00:00
|
|
|
|
{
|
2018-11-10 18:15:30 +00:00
|
|
|
|
CancellationTokenSource cancellationSource = new CancellationTokenSource();
|
|
|
|
|
CancellationToken cancellationToken = cancellationSource.Token;
|
2018-11-10 18:31:58 +00:00
|
|
|
|
Reminder nextReminder = reminderList.GetNextReminder();
|
|
|
|
|
reminderList.OnReminderListUpdate += (object sender, Reminder newReminder) => {
|
|
|
|
|
Reminder newNextReminder = reminderList.GetNextReminder();
|
2018-11-10 18:15:30 +00:00
|
|
|
|
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) {
|
2018-11-10 18:31:58 +00:00
|
|
|
|
nextWaitingTime = nextReminder.Time - DateTime.Now;
|
2018-11-10 18:15:30 +00:00
|
|
|
|
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)"
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-11-10 18:31:58 +00:00
|
|
|
|
reminderList.DeleteReminder(nextReminder);
|
|
|
|
|
reminderList.Save(ReminderFilePath);
|
|
|
|
|
nextReminder = reminderList.GetNextReminder();
|
2018-11-10 01:19:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-10 18:15:30 +00:00
|
|
|
|
|
|
|
|
|
#endregion
|
2018-11-10 01:19:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|