RhinoReminds/RhinoReminds/ClientListener.cs

295 lines
9.9 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using RhinoReminds.Utilities;
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 readonly List<string> AllowedDomains = new List<string>();
public string ReminderFilePath { get; set; } = "./reminders.xml";
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 = ReminderList.FromXmlFile(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)
{
if (!AllowedDomains.Contains("*") && !AllowedDomains.Contains(from.Domain)) {
sendChatMessage(from, "Sorry! The domain of your JID doesn't match the ones in my allowed list.");
return false;
}
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)
{
if (!AllowedDomains.Contains("*") && !AllowedDomains.Contains(message.From.Domain)) {
sendChatMessage(message.From, "Sorry! The domain of your JID doesn't match the ones in my allowed list.");
return;
}
string messageText = message.Body;
string[] parts = Regex.Split(messageText.Trim(), @"\s+");
string instruction = parts[0].ToLower();
switch (parts[0].ToLower())
{
case "help":
sendChatReply(message, "Hello! I'm a reminder bot written by Starbeamrainbowlabs.");
sendChatReply(message, "I can understand messages you send me in regular english.");
sendChatReply(message, "I figure out what you want me to do by looking at the " +
"first word you say, and how you want me to do it by using my AI.");
sendChatReply(message, "I currently understand the following instructions:\n");
sendChatReply(message, "**Remind:** Set a reminder");
sendChatReply(message, "**List / Show:** List the reminders I have set");
sendChatReply(message, "**Delete / Remove:** Delete a reminder by it's number (find this in the reminder list from the instruction above)");
sendChatReply(message, "\nExample: 'Remind me to feed the cat tomorrow at 6pm'");
break;
case "delete":
case "remove":
List<int> failed = new List<int>(),
succeeded = new List<int>();
foreach (int nextId in AIRecogniser.RecogniseNumbers(message.Body)) {
Reminder nextReminder = reminderList.GetById(nextId);
if (nextReminder.JidObj != message.From.GetBareJid()) {
failed.Add(nextId);
continue;
}
reminderList.DeleteReminder(nextReminder);
succeeded.Add(nextId);
}
if (failed.Count > 0) {
string response = string.Join(", ", failed.Select((int nextId) => $"#{nextId}"));
response = $"Sorry! I can't delete reminder{(failed.Count != 1 ? "s" : "")} {response}, as you didn't create {(failed.Count != 1 ? "them":"it")}.";
sendChatReply(message, response);
}
if (succeeded.Count > 0) {
string response = string.Join(", ", succeeded.Select((int nextId) => $"#{nextId}"));
response = $"Deleted reminder{(succeeded.Count != 1 ? "s" : "")} {response} successfully.";
sendChatReply(message, response);
}
break;
case "list":
case "show":
// Filter by reminders for this user.
IEnumerable<Reminder> userReminderList = reminderList.Reminders.Values.Where(
(Reminder next) => message.From.GetBareJid() == next.JidObj.GetBareJid()
);
StringBuilder listMessage = new StringBuilder("I've got the following reminders on my list:\n");
foreach (Reminder nextReminder in userReminderList) {
listMessage.AppendLine($"#{nextReminder.Id}: {nextReminder.Message} at {nextReminder.Time}");
}
listMessage.AppendLine();
listMessage.AppendLine($"({userReminderList.Count()} total)");
sendChatReply(message, listMessage.ToString());
break;
case "remind":
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 = messageText.Remove(dateStringLocation.Min, dateStringLocation.Stride)
.ReplaceMultiple(new string[] {
@"\s{2,}",
@"^remind\s+(?:me\s+)?",
@"^me\s+",
@"^on\s+",
@"my",
}, new string[] {
" ",
"",
"",
"",
"your"
}, RegexOptions.IgnoreCase).Trim();
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}.".Trim().Replace(@"\s+", " ")
);
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
}
}