diff --git a/RhinoReminds/ClientListener.cs b/RhinoReminds/ClientListener.cs index 3403eae..f51b7da 100644 --- a/RhinoReminds/ClientListener.cs +++ b/RhinoReminds/ClientListener.cs @@ -24,9 +24,9 @@ namespace RhinoReminds public event OnConnectedHandler OnConnected; - public readonly string Jid; - public string Username => Jid.Split('@')[0]; - public string Hostname => Jid.Split('@')[1]; + public readonly Jid Jid; + public string Username => Jid.Node; + public string Hostname => Jid.Domain; private readonly string password; public readonly List AllowedDomains = new List(); @@ -35,12 +35,11 @@ namespace RhinoReminds private ReminderList reminderList = new ReminderList(); - private XmppClient client; + private SimpleXmppClient client; /// - /// The number of seconds to wait before trying to reconnect to the + /// The initial number of seconds to wait before trying to reconnect to the /// server again if we loose our connection again. /// - private int nextBackoffDelay = 1; private int defaultBackoffDelay = 1; private float backoffDelayMultiplier = 2; /// @@ -52,7 +51,7 @@ namespace RhinoReminds public ClientListener(string inJid, string inPassword) { - Jid = inJid; + Jid = new Jid(inJid); password = inPassword; } @@ -64,26 +63,48 @@ namespace RhinoReminds 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 await connect(); - //client.SetStatus(Availability.Online); + client.SetStatus(Availability.Online); await watchForReminders(); } + private async Task reconnect() + { + // 1: Ensure we're disconnected from the server. + disconnect(); + + float nextBackoffDelay = defaultBackoffDelay; + + do { + nextBackoffDelay *= backoffDelayMultiplier; + + Console.Error.WriteLine($"[Rhino/Reconnect] Reconnecting in {TimeSpan.FromSeconds(nextBackoffDelay).ToString()}."); + Thread.Sleep((int)(nextBackoffDelay * 1000)); + Console.WriteLine("[Rhino/Reconnect] Attempting to reconnect to the server"); + } while (!await connect()); + } + private async Task connect() { - if (client.Connected) - return true; + if (client != null) { + if (client.Connected) { + return true; + } else { + client.Dispose(); + client = null; + } + } DateTime startTime = DateTime.Now; - client.Connect(); + client = new SimpleXmppClient(Jid, password); + client.Error += errorHandler; + client.Message += messageHandlerRoot; + client.SubscriptionRequest += subscriptionRequestHandler; + + client.Connect("RhinoReminds"); while (!client.Connected) { @@ -99,12 +120,20 @@ namespace RhinoReminds return true; } + private void disconnect() + { + client.Close(); + client.Dispose(); + client = null; + Console.WriteLine($"[Rhino] Disconnected from server."); + } + #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."); + client.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}"); @@ -115,17 +144,8 @@ namespace RhinoReminds { Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}"); - if(!client.Connected || e.Exception is IOException) - { - Console.Error.WriteLine($"[Error/Handler] Reconnecting in {TimeSpan.FromSeconds(nextBackoffDelay).ToString()}."); - Thread.Sleep(nextBackoffDelay * 1000); - Console.WriteLine("[Error/Handler] Attempting to reconnect to the server"); - - if (!connect().Result) - nextBackoffDelay = (int)Math.Ceiling(nextBackoffDelay * backoffDelayMultiplier); - else - nextBackoffDelay = defaultBackoffDelay; - + if(!client.Connected || e.Exception is IOException) { + reconnect().Wait(); } } @@ -140,8 +160,8 @@ namespace RhinoReminds 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) "); + client.SendChatReply(eventArgs.Message, "Oops! I encountered an error. Please report this to my operator!"); + client.SendChatReply(eventArgs.Message, $"Technical details: {WebUtility.HtmlEncode(error.ToString())} (stack trace available in server log) "); } } @@ -160,7 +180,7 @@ namespace RhinoReminds 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."); + client.SendChatMessage(message.From, "Sorry! The domain of your JID doesn't match the ones in my allowed list."); return; } @@ -172,15 +192,15 @@ namespace RhinoReminds 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 " + + client.SendChatReply(message, "Hello! I'm a reminder bot written by Starbeamrainbowlabs."); + client.SendChatReply(message, "I can understand messages you send me in regular english."); + client.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'"); + client.SendChatReply(message, "I currently understand the following instructions:\n"); + client.SendChatReply(message, "**Remind:** Set a reminder"); + client.SendChatReply(message, "**List / Show:** List the reminders I have set"); + client.SendChatReply(message, "**Delete / Remove:** Delete a reminder by it's number (find this in the reminder list from the instruction above)"); + client.SendChatReply(message, "\nExample: 'Remind me to feed the cat tomorrow at 6pm'"); break; case "delete": @@ -201,12 +221,12 @@ namespace RhinoReminds 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); + client.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); + client.SendChatReply(message, response); } break; @@ -222,7 +242,7 @@ namespace RhinoReminds } listMessage.AppendLine(); listMessage.AppendLine($"({userReminderList.Count()} total)"); - sendChatReply(message, listMessage.ToString()); + client.SendChatReply(message, listMessage.ToString()); break; @@ -231,8 +251,8 @@ namespace RhinoReminds 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})"); + client.SendChatReply(message, "Sorry, I had trouble figuring out when you wanted reminding about that!"); + client.SendChatReply(message, $"(Technical details: {error.Message})"); return; } Range dateStringLocation = new Range( @@ -260,45 +280,20 @@ namespace RhinoReminds @"and" }, RegexOptions.IgnoreCase).Trim(); - sendChatReply(message, $"Ok! I'll remind you {reminder} at {dateTime}."); + client.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."); + client.SendChatReply(message, "I don't understand that. Try rephrasing it or asking for help."); break; } } #region Outgoing - /// - /// Sends a chat message to the specified JID. - /// - /// The JID to send the message to. - /// The messaage to send. - private void sendChatMessage(Jid to, string message) { - //Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}"); - client.SendMessage( - to, message, - null, null, MessageType.Chat - ); - } - /// - /// Sends a chat message in direct reply to a given incoming message. - /// - /// Original message. - /// Reply. - 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 @@ -353,17 +348,17 @@ namespace RhinoReminds try { - sendChatMessage( + client.SendChatMessage( nextReminder.Jid, $"Hello! You asked me to remind you {nextReminder.Message} at {nextReminder.Time}.".Trim().Replace(@"\s+", " ") ); } catch (Exception error) { Console.Error.WriteLine($"[Rhino/Reminderd] Caught error sending message to client: {error}"); Console.Error.WriteLine($"[Rhink/Reminderd] Offending reminder: {nextReminder}"); - sendChatMessage(nextReminder.Jid, "Oops! I encountered an error sending you a reminder. Please contact my operator!"); + client.SendChatMessage(nextReminder.Jid, "Oops! I encountered an error sending you a reminder. Please contact my operator!"); } if (nextWaitingTime.TotalMilliseconds < 0) { - sendChatMessage( + client.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)" diff --git a/RhinoReminds/Program.cs b/RhinoReminds/Program.cs index 5a13ccb..cd588da 100644 --- a/RhinoReminds/Program.cs +++ b/RhinoReminds/Program.cs @@ -86,10 +86,13 @@ namespace RhinoReminds client.AllowedDomains.Add(settings.AllowedDomain); // Update the avatar if appropriate if (settings.AvatarFilepath != string.Empty) { - client.OnConnected += (object sender, OnConnectedEventArgs eventArgs) => { + OnConnectedHandler handler = null; + handler = (object sender, OnConnectedEventArgs eventArgs) => { client.SetAvatar(settings.AvatarFilepath); Console.WriteLine($"[Program] Set avatar to '{settings.AvatarFilepath}'."); + client.OnConnected -= handler; }; + client.OnConnected += handler; } // Connect to the server & start listening // Make sure the program doesn't exit whilst we're connected diff --git a/RhinoReminds/RhinoReminds.csproj b/RhinoReminds/RhinoReminds.csproj index b851a2d..032abae 100644 --- a/RhinoReminds/RhinoReminds.csproj +++ b/RhinoReminds/RhinoReminds.csproj @@ -68,6 +68,7 @@ + diff --git a/RhinoReminds/SimpleXmppClient.cs b/RhinoReminds/SimpleXmppClient.cs new file mode 100644 index 0000000..4c180f6 --- /dev/null +++ b/RhinoReminds/SimpleXmppClient.cs @@ -0,0 +1,42 @@ +using System; +using S22.Xmpp; +using S22.Xmpp.Client; +using S22.Xmpp.Im; + +namespace RhinoReminds +{ + public class SimpleXmppClient : XmppClient + { + public SimpleXmppClient(Jid user, string password) : base(user.Domain, user.Node, password) + { + } + + /// + /// Sends a chat message to the specified JID. + /// + /// The JID to send the message to. + /// The messaage to send. + public void SendChatMessage(Jid to, string message) + { + //Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}"); + SendMessage( + to, message, + null, null, MessageType.Chat + ); + } + /// + /// Sends a chat message in direct reply to a given incoming message. + /// + /// Original message. + /// Reply. + public void SendChatReply(Message originalMessage, string reply) + { + //Console.WriteLine($"[Rhino/Send/Reply] Sending {reply} -> {originalMessage.From}"); + SendMessage( + originalMessage.From, reply, + null, originalMessage.Thread, MessageType.Chat + ); + } + + } +}