Fix the reconnection logic. Hooray!

This commit is contained in:
Starbeamrainbowlabs 2018-12-21 10:58:14 +00:00
parent 505e500634
commit 4174ec85c6
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
4 changed files with 116 additions and 75 deletions

View file

@ -24,9 +24,9 @@ namespace RhinoReminds
public event OnConnectedHandler OnConnected; public event OnConnectedHandler OnConnected;
public readonly string Jid; public readonly Jid Jid;
public string Username => Jid.Split('@')[0]; public string Username => Jid.Node;
public string Hostname => Jid.Split('@')[1]; public string Hostname => Jid.Domain;
private readonly string password; private readonly string password;
public readonly List<string> AllowedDomains = new List<string>(); public readonly List<string> AllowedDomains = new List<string>();
@ -35,12 +35,11 @@ namespace RhinoReminds
private ReminderList reminderList = new ReminderList(); private ReminderList reminderList = new ReminderList();
private XmppClient client; private SimpleXmppClient client;
/// <summary> /// <summary>
/// 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. /// server again if we loose our connection again.
/// </summary> /// </summary>
private int nextBackoffDelay = 1;
private int defaultBackoffDelay = 1; private int defaultBackoffDelay = 1;
private float backoffDelayMultiplier = 2; private float backoffDelayMultiplier = 2;
/// <summary> /// <summary>
@ -52,7 +51,7 @@ namespace RhinoReminds
public ClientListener(string inJid, string inPassword) public ClientListener(string inJid, string inPassword)
{ {
Jid = inJid; Jid = new Jid(inJid);
password = inPassword; password = inPassword;
} }
@ -64,26 +63,48 @@ namespace RhinoReminds
reminderList = ReminderList.FromXmlFile(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 // Connect to the server. This starts it's own thread that doesn't block the program exiting, apparently
await connect(); await connect();
//client.SetStatus(Availability.Online); client.SetStatus(Availability.Online);
await watchForReminders(); 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<bool> connect() private async Task<bool> connect()
{ {
if (client.Connected) if (client != null) {
return true; if (client.Connected) {
return true;
} else {
client.Dispose();
client = null;
}
}
DateTime startTime = DateTime.Now; 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) while (!client.Connected)
{ {
@ -99,12 +120,20 @@ namespace RhinoReminds
return true; return true;
} }
private void disconnect()
{
client.Close();
client.Dispose();
client = null;
Console.WriteLine($"[Rhino] Disconnected from server.");
}
#region XMPP Event Handling #region XMPP Event Handling
private bool subscriptionRequestHandler(Jid from) private bool subscriptionRequestHandler(Jid from)
{ {
if (!AllowedDomains.Contains("*") && !AllowedDomains.Contains(from.Domain)) { 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; return false;
} }
Console.WriteLine($"[Rhino/SubscriptionRequest] Approving subscription from {from}"); Console.WriteLine($"[Rhino/SubscriptionRequest] Approving subscription from {from}");
@ -115,17 +144,8 @@ namespace RhinoReminds
{ {
Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}"); Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}");
if(!client.Connected || e.Exception is IOException) if(!client.Connected || e.Exception is IOException) {
{ reconnect().Wait();
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;
} }
} }
@ -140,8 +160,8 @@ namespace RhinoReminds
catch (Exception error) catch (Exception error)
{ {
Console.Error.WriteLine(error); Console.Error.WriteLine(error);
sendChatReply(eventArgs.Message, "Oops! I encountered an error. Please report this to my operator!"); client.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, $"Technical details: {WebUtility.HtmlEncode(error.ToString())} (stack trace available in server log) ");
} }
} }
@ -160,7 +180,7 @@ namespace RhinoReminds
private void messageHandler(Message message) private void messageHandler(Message message)
{ {
if (!AllowedDomains.Contains("*") && !AllowedDomains.Contains(message.From.Domain)) { 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; return;
} }
@ -172,15 +192,15 @@ namespace RhinoReminds
switch (parts[0].ToLower()) switch (parts[0].ToLower())
{ {
case "help": case "help":
sendChatReply(message, "Hello! I'm a reminder bot written by Starbeamrainbowlabs."); client.SendChatReply(message, "Hello! I'm a reminder bot written by Starbeamrainbowlabs.");
sendChatReply(message, "I can understand messages you send me in regular english."); client.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, "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."); "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"); client.SendChatReply(message, "I currently understand the following instructions:\n");
sendChatReply(message, "**Remind:** Set a reminder"); client.SendChatReply(message, "**Remind:** Set a reminder");
sendChatReply(message, "**List / Show:** List the reminders I have set"); client.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)"); client.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, "\nExample: 'Remind me to feed the cat tomorrow at 6pm'");
break; break;
case "delete": case "delete":
@ -201,12 +221,12 @@ namespace RhinoReminds
if (failed.Count > 0) { if (failed.Count > 0) {
string response = string.Join(", ", failed.Select((int nextId) => $"#{nextId}")); 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")}."; 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) { if (succeeded.Count > 0) {
string response = string.Join(", ", succeeded.Select((int nextId) => $"#{nextId}")); string response = string.Join(", ", succeeded.Select((int nextId) => $"#{nextId}"));
response = $"Deleted reminder{(succeeded.Count != 1 ? "s" : "")} {response} successfully."; response = $"Deleted reminder{(succeeded.Count != 1 ? "s" : "")} {response} successfully.";
sendChatReply(message, response); client.SendChatReply(message, response);
} }
break; break;
@ -222,7 +242,7 @@ namespace RhinoReminds
} }
listMessage.AppendLine(); listMessage.AppendLine();
listMessage.AppendLine($"({userReminderList.Count()} total)"); listMessage.AppendLine($"({userReminderList.Count()} total)");
sendChatReply(message, listMessage.ToString()); client.SendChatReply(message, listMessage.ToString());
break; break;
@ -231,8 +251,8 @@ namespace RhinoReminds
try { try {
dateTime = AIRecogniser.RecogniseDateTime(messageText, out rawDateTimeString); dateTime = AIRecogniser.RecogniseDateTime(messageText, out rawDateTimeString);
} catch (AIException error) { } catch (AIException error) {
sendChatReply(message, "Sorry, I had trouble figuring out when you wanted reminding about that!"); client.SendChatReply(message, "Sorry, I had trouble figuring out when you wanted reminding about that!");
sendChatReply(message, $"(Technical details: {error.Message})"); client.SendChatReply(message, $"(Technical details: {error.Message})");
return; return;
} }
Range dateStringLocation = new Range( Range dateStringLocation = new Range(
@ -260,45 +280,20 @@ namespace RhinoReminds
@"and" @"and"
}, RegexOptions.IgnoreCase).Trim(); }, 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); Reminder newReminder = reminderList.CreateReminder(message.From, dateTime, reminder);
reminderList.Save(ReminderFilePath); reminderList.Save(ReminderFilePath);
break; break;
default: 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; break;
} }
} }
#region Outgoing #region Outgoing
/// <summary>
/// Sends a chat message to the specified JID.
/// </summary>
/// <param name="to">The JID to send the message to.</param>
/// <param name="message">The messaage to send.</param>
private void sendChatMessage(Jid to, string message) {
//Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}");
client.SendMessage(
to, message,
null, null, MessageType.Chat
);
}
/// <summary>
/// Sends a chat message in direct reply to a given incoming message.
/// </summary>
/// <param name="originalMessage">Original message.</param>
/// <param name="reply">Reply.</param>
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 #endregion
@ -353,17 +348,17 @@ namespace RhinoReminds
try try
{ {
sendChatMessage( client.SendChatMessage(
nextReminder.Jid, nextReminder.Jid,
$"Hello! You asked me to remind you {nextReminder.Message} at {nextReminder.Time}.".Trim().Replace(@"\s+", " ") $"Hello! You asked me to remind you {nextReminder.Message} at {nextReminder.Time}.".Trim().Replace(@"\s+", " ")
); );
} catch (Exception error) { } catch (Exception error) {
Console.Error.WriteLine($"[Rhino/Reminderd] Caught error sending message to client: {error}"); Console.Error.WriteLine($"[Rhino/Reminderd] Caught error sending message to client: {error}");
Console.Error.WriteLine($"[Rhink/Reminderd] Offending reminder: {nextReminder}"); 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) { if (nextWaitingTime.TotalMilliseconds < 0) {
sendChatMessage( client.SendChatMessage(
nextReminder.Jid, nextReminder.Jid,
"(Sorry I'm late reminding you! I might not have been running at the time, " + "(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)" "or you might have scheduled a reminder for the past)"

View file

@ -86,10 +86,13 @@ namespace RhinoReminds
client.AllowedDomains.Add(settings.AllowedDomain); client.AllowedDomains.Add(settings.AllowedDomain);
// Update the avatar if appropriate // Update the avatar if appropriate
if (settings.AvatarFilepath != string.Empty) { if (settings.AvatarFilepath != string.Empty) {
client.OnConnected += (object sender, OnConnectedEventArgs eventArgs) => { OnConnectedHandler handler = null;
handler = (object sender, OnConnectedEventArgs eventArgs) => {
client.SetAvatar(settings.AvatarFilepath); client.SetAvatar(settings.AvatarFilepath);
Console.WriteLine($"[Program] Set avatar to '{settings.AvatarFilepath}'."); Console.WriteLine($"[Program] Set avatar to '{settings.AvatarFilepath}'.");
client.OnConnected -= handler;
}; };
client.OnConnected += handler;
} }
// Connect to the server & start listening // Connect to the server & start listening
// Make sure the program doesn't exit whilst we're connected // Make sure the program doesn't exit whilst we're connected

View file

@ -68,6 +68,7 @@
<Compile Include="Exceptions.cs" /> <Compile Include="Exceptions.cs" />
<Compile Include="Utilities\Range.cs" /> <Compile Include="Utilities\Range.cs" />
<Compile Include="Utilities\TextHelpers.cs" /> <Compile Include="Utilities\TextHelpers.cs" />
<Compile Include="SimpleXmppClient.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View file

@ -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)
{
}
/// <summary>
/// Sends a chat message to the specified JID.
/// </summary>
/// <param name="to">The JID to send the message to.</param>
/// <param name="message">The messaage to send.</param>
public void SendChatMessage(Jid to, string message)
{
//Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}");
SendMessage(
to, message,
null, null, MessageType.Chat
);
}
/// <summary>
/// Sends a chat message in direct reply to a given incoming message.
/// </summary>
/// <param name="originalMessage">Original message.</param>
/// <param name="reply">Reply.</param>
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
);
}
}
}