Merge branch 'master' of git.starbeamrainbowlabs.com:sbrl/RhinoReminds
This commit is contained in:
commit
6f9ff7eef0
11 changed files with 427 additions and 170 deletions
27
README.md
27
README.md
|
@ -2,7 +2,14 @@
|
||||||
|
|
||||||
> An XMPP reminder bot written in C#.
|
> An XMPP reminder bot written in C#.
|
||||||
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
### Downloading Prebuilt Binaries
|
||||||
|
Prebuilt binaries for the latest release are available on the [releases page](https://git.starbeamrainbowlabs.com/sbrl/RhinoReminds/releases).
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
1. Install the NuGet dependencies:
|
1. Install the NuGet dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -32,6 +39,7 @@ mono [--debug] RhinoReminds.exe [--help]
|
||||||
RhinoReminds.exe [--help]
|
RhinoReminds.exe [--help]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
The bot operates on natural language instructions. It picks what to do from the first word in the sentence, but the rest is parsed via AI.
|
The bot operates on natural language instructions. It picks what to do from the first word in the sentence, but the rest is parsed via AI.
|
||||||
|
|
||||||
|
@ -52,6 +60,25 @@ Show all
|
||||||
List all
|
List all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Deleting Reminders
|
||||||
|
|
||||||
|
```
|
||||||
|
Delete reminder 43
|
||||||
|
Delete #22, #23, and #45
|
||||||
|
Delete number eight
|
||||||
|
Delete reminders 2, 3, 4, and 7
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Contributions are welcome! Bug reports can be opened against this repository if you have an account. Otherwise, send them to `bugs at starbeamrainbowlabs dot com`.
|
||||||
|
|
||||||
|
Pull requests and patches are welcome too. [Here's a great tutorial](https://makandracards.com/makandra/2521-git-how-to-create-and-apply-patches) on creating patches. If there's any interest, I'll move this repository to my account on [gitlab.com](https://gitlab.com/sbrl) if that makes things easier.
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
RhinoReminds is licensed under the _Mozilla Public License 2.0_ (MPL-2.0 for short) - the full text of which can be found in the [LICENSE](https://git.starbeamrainbowlabs.com/sbrl/RhinoReminds/src/branch/master/LICENSE) file in this repository. tl;drLegal have a [great summary](https://tldrlegal.com/license/mozilla-public-license-2.0-(mpl-2)) if you don't want to spend all day read dry legalese :P
|
||||||
|
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
- [Microsoft.Text.Recognizers Samples](https://github.com/Microsoft/Recognizers-Text/tree/master/.NET/Samples)
|
- [Microsoft.Text.Recognizers Samples](https://github.com/Microsoft/Recognizers-Text/tree/master/.NET/Samples)
|
||||||
|
|
|
@ -9,45 +9,49 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using RhinoReminds.Utilities;
|
using RhinoReminds.Utilities;
|
||||||
using S22.Xmpp;
|
using S22.Xmpp;
|
||||||
using S22.Xmpp.Client;
|
|
||||||
using S22.Xmpp.Im;
|
using S22.Xmpp.Im;
|
||||||
using SBRL.Geometry;
|
using SBRL.Geometry;
|
||||||
|
|
||||||
namespace RhinoReminds
|
namespace RhinoReminds
|
||||||
{
|
{
|
||||||
|
public class OnConnectedEventArgs : EventArgs { }
|
||||||
|
public delegate void OnConnectedHandler(object sender, OnConnectedEventArgs eventArgs);
|
||||||
|
|
||||||
public class ClientListener
|
public class ClientListener
|
||||||
{
|
{
|
||||||
public bool Debug { get; set; } = false;
|
public bool Debug { get; set; } = false;
|
||||||
|
|
||||||
public readonly string Jid;
|
public event OnConnectedHandler OnConnected;
|
||||||
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;
|
private readonly string password;
|
||||||
|
|
||||||
public readonly List<string> AllowedDomains = new List<string>();
|
public readonly List<string> AllowedDomains = new List<string>();
|
||||||
|
|
||||||
public string ReminderFilePath { get; set; } = "./reminders.xml";
|
public string ReminderFilePath { get; set; } = "./reminders.xml";
|
||||||
private ReminderList reminderList = new ReminderList();
|
private ReminderList reminderList = new ReminderList();
|
||||||
|
private CancellationTokenSource reminderWatcherReset;
|
||||||
|
private CancellationToken reminderWatcherResetToken => reminderWatcherReset.Token;
|
||||||
|
|
||||||
|
private SimpleXmppClient client;
|
||||||
private XmppClient 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 readonly int defaultBackoffDelay = 1;
|
||||||
private int defaultBackoffDelay = 1;
|
private readonly float backoffDelayMultiplier = 2;
|
||||||
private float backoffDelayMultiplier = 2;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If a connection attempt doesn't succeed in this number of seconds,
|
/// If a connection attempt doesn't succeed in this number of seconds,
|
||||||
/// give up and try again later.
|
/// give up and try again later.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private int giveUpTimeout = 30;
|
private readonly int giveUpTimeout = 30;
|
||||||
|
|
||||||
|
|
||||||
public ClientListener(string inJid, string inPassword)
|
public ClientListener(string inJid, string inPassword)
|
||||||
{
|
{
|
||||||
Jid = inJid;
|
Jid = new Jid(inJid);
|
||||||
password = inPassword;
|
password = inPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,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)
|
||||||
{
|
{
|
||||||
|
@ -89,16 +115,25 @@ namespace RhinoReminds
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"[Rhino/Setup] Connected as {Jid}");
|
Console.WriteLine($"[Rhino/Setup] Connected as {Jid}");
|
||||||
|
OnConnected(this, new OnConnectedEventArgs());
|
||||||
|
|
||||||
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}");
|
||||||
|
@ -109,17 +144,8 @@ namespace RhinoReminds
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}");
|
Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}");
|
||||||
|
|
||||||
if(!client.Connected)
|
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,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) ");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,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":
|
||||||
|
@ -195,19 +221,21 @@ 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) {
|
||||||
|
// Ensure that the reminder thread picks up the changes
|
||||||
|
interruptReminderWatcher();
|
||||||
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;
|
||||||
|
|
||||||
case "list":
|
case "list":
|
||||||
case "show":
|
case "show":
|
||||||
// Filter by reminders for this user.
|
// Filter by reminders for this user.
|
||||||
IEnumerable<Reminder> userReminderList = reminderList.Reminders.Values.Where(
|
IEnumerable<Reminder> userReminderList = reminderList.Reminders.Where(
|
||||||
(Reminder next) => message.From.GetBareJid() == next.JidObj.GetBareJid()
|
(Reminder next) => message.From.GetBareJid() == next.JidObj.GetBareJid()
|
||||||
);
|
);
|
||||||
StringBuilder listMessage = new StringBuilder("I've got the following reminders on my list:\n");
|
StringBuilder listMessage = new StringBuilder("I've got the following reminders on my list:\n");
|
||||||
|
@ -216,7 +244,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;
|
||||||
|
|
||||||
|
@ -225,8 +253,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(
|
||||||
|
@ -239,70 +267,63 @@ namespace RhinoReminds
|
||||||
@"^remind\s+(?:me\s+)?",
|
@"^remind\s+(?:me\s+)?",
|
||||||
@"^me\s+",
|
@"^me\s+",
|
||||||
@"^on\s+",
|
@"^on\s+",
|
||||||
@"my",
|
|
||||||
@"you",
|
@"you",
|
||||||
@"your",
|
@"your",
|
||||||
|
@"my",
|
||||||
@"&" // Ampersands cause a crash when sending!
|
@"&" // Ampersands cause a crash when sending!
|
||||||
}, new string[] {
|
}, new string[] {
|
||||||
" ",
|
" ",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"your",
|
|
||||||
@"me",
|
@"me",
|
||||||
@"my",
|
@"my",
|
||||||
|
"your",
|
||||||
@"and"
|
@"and"
|
||||||
}, RegexOptions.IgnoreCase).Trim();
|
}, RegexOptions.IgnoreCase).Trim();
|
||||||
|
|
||||||
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);
|
||||||
|
if (newReminder == null) {
|
||||||
|
client.SendChatReply(
|
||||||
|
message,
|
||||||
|
"Oops! It looks like you've already got a reminder idential "
|
||||||
|
+ "to that one set, so I wasn't able to set a reminder for you. Please contact my "
|
||||||
|
+ "operator, as this is probably a bug."
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SendChatReply(message, $"Ok! I'll remind you {reminder} at {dateTime}.");
|
||||||
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
|
||||||
|
|
||||||
#region Reminder Listening
|
#region Reminder Listening
|
||||||
|
|
||||||
|
private void interruptReminderWatcher()
|
||||||
|
{
|
||||||
|
reminderWatcherReset?.Cancel();
|
||||||
|
reminderWatcherReset = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task watchForReminders()
|
private async Task watchForReminders()
|
||||||
{
|
{
|
||||||
CancellationTokenSource cancellationSource = new CancellationTokenSource();
|
interruptReminderWatcher();
|
||||||
CancellationToken cancellationToken = cancellationSource.Token;
|
|
||||||
Reminder nextReminder = reminderList.GetNextReminder();
|
Reminder nextReminder = reminderList.GetNextReminder();
|
||||||
|
|
||||||
|
// ----- Events -----
|
||||||
|
// This will run on the firing thread, not on this thread
|
||||||
reminderList.OnReminderListUpdate += (object sender, Reminder newReminder) => {
|
reminderList.OnReminderListUpdate += (object sender, Reminder newReminder) => {
|
||||||
Reminder newNextReminder = reminderList.GetNextReminder();
|
Reminder newNextReminder = reminderList.GetNextReminder();
|
||||||
//Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing.");
|
//Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing.");
|
||||||
|
@ -310,65 +331,76 @@ namespace RhinoReminds
|
||||||
if (nextReminder != newNextReminder) {
|
if (nextReminder != newNextReminder) {
|
||||||
//Console.WriteLine($"[Rhino/Reminderd/Canceller] Cancelling");
|
//Console.WriteLine($"[Rhino/Reminderd/Canceller] Cancelling");
|
||||||
nextReminder = newNextReminder;
|
nextReminder = newNextReminder;
|
||||||
cancellationSource.Cancel();
|
interruptReminderWatcher();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// ------------------
|
||||||
|
|
||||||
while (true)
|
while (true) {
|
||||||
{
|
nextReminder = reminderList.GetNextReminder();
|
||||||
|
// Wait for the next reminder
|
||||||
TimeSpan nextWaitingTime;
|
TimeSpan nextWaitingTime;
|
||||||
try {
|
try {
|
||||||
if (nextReminder != null) {
|
if (nextReminder != null) {
|
||||||
nextWaitingTime = nextReminder.Time - DateTime.Now;
|
nextWaitingTime = nextReminder.Time - DateTime.Now;
|
||||||
if (DateTime.Now < nextReminder.Time) {
|
if (DateTime.Now < nextReminder.Time)
|
||||||
|
{
|
||||||
//Console.WriteLine($"[Rhino/Reminderd] Sleeping for {nextWaitingTime}");
|
//Console.WriteLine($"[Rhino/Reminderd] Sleeping for {nextWaitingTime}");
|
||||||
await Task.Delay(nextWaitingTime, cancellationToken);
|
await Task.Delay(nextWaitingTime, reminderWatcherResetToken);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
//Console.WriteLine("[Rhino/Reminderd] Sleeping until interrupted");
|
|
||||||
await Task.Delay(Timeout.Infinite, cancellationToken);
|
|
||||||
}
|
}
|
||||||
} catch (TaskCanceledException) {
|
else {
|
||||||
|
//Console.WriteLine("[Rhino/Reminderd] Sleeping until interrupted");
|
||||||
|
await Task.Delay(Timeout.Infinite, reminderWatcherResetToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
//Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating");
|
//Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating");
|
||||||
cancellationSource = new CancellationTokenSource();
|
interruptReminderWatcher();
|
||||||
cancellationToken = cancellationSource.Token;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested) {
|
if (reminderWatcherResetToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating (but no exception thrown)");
|
Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating (but no exception thrown)");
|
||||||
cancellationSource = new CancellationTokenSource();
|
interruptReminderWatcher();
|
||||||
cancellationToken = cancellationSource.Token;
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"[Rhino/Reminderd] Sending notification {nextReminder}");
|
Console.WriteLine($"[Rhino/Reminderd] Sending notification {nextReminder}");
|
||||||
|
sendAndDeleteReminder(nextReminder);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
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!");
|
|
||||||
}
|
|
||||||
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)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
reminderList.DeleteReminder(nextReminder);
|
|
||||||
reminderList.Save(ReminderFilePath);
|
|
||||||
nextReminder = reminderList.GetNextReminder();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendAndDeleteReminder(Reminder nextReminder)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
client.SendChatMessage(nextReminder.Jid, "Oops! I encountered an error sending you a reminder. Please contact my operator!");
|
||||||
|
}
|
||||||
|
|
||||||
|
reminderList.DeleteReminder(nextReminder);
|
||||||
|
reminderList.Save(ReminderFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
RhinoReminds/CompareReminders.cs
Normal file
17
RhinoReminds/CompareReminders.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace RhinoReminds
|
||||||
|
{
|
||||||
|
public class CompareReminders : IComparer<Reminder>
|
||||||
|
{
|
||||||
|
public CompareReminders()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(Reminder x, Reminder y)
|
||||||
|
{
|
||||||
|
return (int)(x.Time - y.Time).TotalMilliseconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using S22.Xmpp;
|
using S22.Xmpp;
|
||||||
|
@ -15,6 +16,7 @@ namespace RhinoReminds
|
||||||
public string Jid = null;
|
public string Jid = null;
|
||||||
|
|
||||||
public string AvatarFilepath = string.Empty;
|
public string AvatarFilepath = string.Empty;
|
||||||
|
public string PidFile = null;
|
||||||
|
|
||||||
public string Password = null;
|
public string Password = null;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +49,8 @@ namespace RhinoReminds
|
||||||
Console.WriteLine(" -h --help Show this message");
|
Console.WriteLine(" -h --help Show this message");
|
||||||
Console.WriteLine($" -f --file Specify where to save reminders (default: {settings.Filepath})");
|
Console.WriteLine($" -f --file Specify where to save reminders (default: {settings.Filepath})");
|
||||||
Console.WriteLine(" --domain {domain} Set the domain users are allowed to originate at. Defaults to any domain.");
|
Console.WriteLine(" --domain {domain} Set the domain users are allowed to originate at. Defaults to any domain.");
|
||||||
Console.WriteLine(" --avatar Update the XMPP account's avatar to the specified image. By default the avatar is not updated.");
|
Console.WriteLine(" --avatar Update the XMPP account's avatar to the specified image. By default the avatar is not updated.");
|
||||||
|
Console.WriteLine(" --pidfile Save our process ID to the specified file, and delete it on exit");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("Environment Variables:");
|
Console.WriteLine("Environment Variables:");
|
||||||
Console.WriteLine(" XMPP_JID The JID to login to");
|
Console.WriteLine(" XMPP_JID The JID to login to");
|
||||||
|
@ -67,30 +70,84 @@ namespace RhinoReminds
|
||||||
case "--avatar":
|
case "--avatar":
|
||||||
settings.AvatarFilepath = args[++i];
|
settings.AvatarFilepath = args[++i];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "--pidfile":
|
||||||
|
settings.PidFile = args[++i];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Console.Error.WriteLine($"Error: Unknown argument '{args[i]}'.");
|
||||||
|
return 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.Jid = Environment.GetEnvironmentVariable("XMPP_JID");
|
settings.Jid = Environment.GetEnvironmentVariable("XMPP_JID");
|
||||||
settings.Password = Environment.GetEnvironmentVariable("XMPP_PASSWORD");
|
settings.Password = Environment.GetEnvironmentVariable("XMPP_PASSWORD");
|
||||||
|
|
||||||
Run();
|
if (settings.Jid == null) {
|
||||||
|
Console.Error.WriteLine("Error: No JID specified to login with.");
|
||||||
|
Console.Error.WriteLine("Do so with the XMPP_JID environment variable!");
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
if (settings.Password == null) {
|
||||||
|
Console.Error.WriteLine("Error: No password specified to login with.");
|
||||||
|
Console.Error.WriteLine("Do so with the XMPP_PASSWORD environment variable!");
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.PidFile != null)
|
||||||
|
setupPidFile();
|
||||||
|
|
||||||
|
run();
|
||||||
|
// We shouldn't ever end up here, but just in case.....
|
||||||
|
cleanupPidFile();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Run()
|
private static void setupPidFile()
|
||||||
|
{
|
||||||
|
File.WriteAllText(settings.PidFile, Process.GetCurrentProcess().Id.ToString());
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => cleanupPidFile();
|
||||||
|
AppDomain.CurrentDomain.DomainUnload += (object sender, EventArgs e) => cleanupPidFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cleanupPidFile() {
|
||||||
|
// Make sure we only do cleanup once
|
||||||
|
if (settings.PidFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
File.Delete(settings.PidFile);
|
||||||
|
settings.PidFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run()
|
||||||
{
|
{
|
||||||
ClientListener client = new ClientListener(settings.Jid, settings.Password) {
|
ClientListener client = new ClientListener(settings.Jid, settings.Password) {
|
||||||
ReminderFilePath = settings.Filepath
|
ReminderFilePath = settings.Filepath
|
||||||
};
|
};
|
||||||
client.AllowedDomains.Add(settings.AllowedDomain);
|
client.AllowedDomains.Add(settings.AllowedDomain);
|
||||||
// Connect to the server & start listening
|
|
||||||
Task clientListener = client.Start();
|
|
||||||
// Update the avatar if appropriate
|
// Update the avatar if appropriate
|
||||||
if(settings.AvatarFilepath != string.Empty)
|
if (settings.AvatarFilepath != string.Empty) {
|
||||||
client.SetAvatar(settings.AvatarFilepath);
|
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
|
// Make sure the program doesn't exit whilst we're connected
|
||||||
clientListener.Wait();
|
try
|
||||||
|
{
|
||||||
|
client.Start().Wait();
|
||||||
|
} catch (Exception) {
|
||||||
|
// Ensure we tidy up after ourselves by deleting the PID file
|
||||||
|
if (settings.PidFile != null)
|
||||||
|
cleanupPidFile();
|
||||||
|
// Re-throw the error
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using S22.Xmpp;
|
using S22.Xmpp;
|
||||||
|
@ -6,29 +7,59 @@ using S22.Xmpp;
|
||||||
namespace RhinoReminds
|
namespace RhinoReminds
|
||||||
{
|
{
|
||||||
|
|
||||||
public class Reminder
|
public class Reminder : IEquatable<Reminder>
|
||||||
{
|
{
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
public string Jid { get; }
|
public string Jid => JidObj.ToString();
|
||||||
public Jid JidObj => new Jid(Jid);
|
public Jid JidObj { get; }
|
||||||
public DateTime Time { get; }
|
public DateTime Time { get; private set; }
|
||||||
public string Message { get; }
|
public string Message { get; }
|
||||||
|
|
||||||
public Reminder(int inId, string inJid, DateTime inTime, string inMessage)
|
public Reminder(int inId, Jid inJid, DateTime inTime, string inMessage)
|
||||||
{
|
{
|
||||||
Id = inId;
|
Id = inId;
|
||||||
Jid = inJid;
|
JidObj = inJid.GetBareJid();
|
||||||
Time = inTime;
|
Time = inTime;
|
||||||
Message = inMessage;
|
Message = inMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public void TweakTime() {
|
||||||
{
|
Time = Time.AddMilliseconds(1);
|
||||||
return $"[Reminder Id={Id}, Jid={Jid}, Time={Time}, Message={Message}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region XML
|
|
||||||
|
|
||||||
|
#region Overrides
|
||||||
|
|
||||||
|
public override bool Equals(object obj) {
|
||||||
|
if (!(obj is Reminder)) return false;
|
||||||
|
Reminder otherReminder = obj as Reminder;
|
||||||
|
return otherReminder.Id == Id &&
|
||||||
|
otherReminder.JidObj == JidObj && // Will *always* be a bare Jid
|
||||||
|
otherReminder.Time == Time &&
|
||||||
|
otherReminder.Message == Message;
|
||||||
|
}
|
||||||
|
// For IEquatable<Reminder> implementation
|
||||||
|
public bool Equals(Reminder otherReminder) => Equals((object)otherReminder);
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
int hashCode = -81903051;
|
||||||
|
hashCode = (hashCode * -1521134295) + Id.GetHashCode();
|
||||||
|
hashCode = (hashCode * -1521134295) + EqualityComparer<Jid>.Default.GetHashCode(JidObj);
|
||||||
|
hashCode = (hashCode * -1521134295) + Time.GetHashCode();
|
||||||
|
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(Message);
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
public static bool operator ==(Reminder reminder1, Reminder reminder2) => EqualityComparer<Reminder>.Default.Equals(reminder1, reminder2);
|
||||||
|
public static bool operator !=(Reminder reminder1, Reminder reminder2) => !(reminder1 == reminder2);
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return $"[Reminder Id={Id}, Jid={Jid}, Time={Time}, Message=\"{Message}\"]";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region XML
|
||||||
|
|
||||||
public void WriteToXml(XmlWriter xml)
|
public void WriteToXml(XmlWriter xml)
|
||||||
{
|
{
|
||||||
|
@ -66,7 +97,8 @@ namespace RhinoReminds
|
||||||
return new Reminder(id, jid, dateTime, message);
|
return new Reminder(id, jid, dateTime, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using S22.Xmpp;
|
using S22.Xmpp;
|
||||||
|
@ -14,7 +13,7 @@ namespace RhinoReminds
|
||||||
private int nextId = 0;
|
private int nextId = 0;
|
||||||
private readonly object saveLock = new object();
|
private readonly object saveLock = new object();
|
||||||
|
|
||||||
public SortedList<DateTime, Reminder> Reminders = new SortedList<DateTime, Reminder>();
|
public SortedSet<Reminder> Reminders = new SortedSet<Reminder>(new CompareReminders());
|
||||||
|
|
||||||
public event OnReminderListUpdateHandler OnReminderListUpdate;
|
public event OnReminderListUpdateHandler OnReminderListUpdate;
|
||||||
|
|
||||||
|
@ -29,29 +28,28 @@ namespace RhinoReminds
|
||||||
{
|
{
|
||||||
Reminder result = new Reminder(nextId++, $"{inJid.Node}@{inJid.Domain}", time, message);
|
Reminder result = new Reminder(nextId++, $"{inJid.Node}@{inJid.Domain}", time, message);
|
||||||
Console.WriteLine($"[Rhino/ReminderList] Created reminder {result}");
|
Console.WriteLine($"[Rhino/ReminderList] Created reminder {result}");
|
||||||
while (Reminders.ContainsKey(time))
|
while (Reminders.Contains(result))
|
||||||
time = time.AddMilliseconds(1);
|
result.TweakTime();
|
||||||
Reminders.Add(time, result);
|
|
||||||
|
Reminders.Add(result);
|
||||||
OnReminderListUpdate(this, result);
|
OnReminderListUpdate(this, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Reminder GetNextReminder()
|
public Reminder GetNextReminder() {
|
||||||
{
|
|
||||||
if (Reminders.Count == 0)
|
if (Reminders.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return Reminders.Values[0];
|
return Reminders.Min;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Reminder GetById(int targetId)
|
public Reminder GetById(int targetId) {
|
||||||
{
|
return Reminders.First((Reminder nextReminder) => nextReminder.Id == targetId);
|
||||||
return Reminders.First((KeyValuePair<DateTime, Reminder> nextPair) => nextPair.Value.Id == targetId).Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteReminder(Reminder nextReminder)
|
public void DeleteReminder(Reminder nextReminder) {
|
||||||
{
|
if (!Reminders.Remove(nextReminder))
|
||||||
Reminders.Remove(nextReminder.Time);
|
throw new ApplicationException($"Error: Failed to remove the reminder {nextReminder} from the list!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,7 +60,7 @@ namespace RhinoReminds
|
||||||
// Make sure that the reminder thread doesn't try to save the reminders at the exact same time
|
// Make sure that the reminder thread doesn't try to save the reminders at the exact same time
|
||||||
// we receive a request for a new reminder
|
// we receive a request for a new reminder
|
||||||
lock (saveLock)
|
lock (saveLock)
|
||||||
{ // FUTURE: We could go lockless here with some work, but it's not worth it for the teeny chance & low overhead
|
{ // FUTURE: We could go lockless here with some work, but it's not worth it for the teensy chance & low overhead
|
||||||
XmlWriter xml = XmlWriter.Create(
|
XmlWriter xml = XmlWriter.Create(
|
||||||
filename,
|
filename,
|
||||||
new XmlWriterSettings() { Indent = true }
|
new XmlWriterSettings() { Indent = true }
|
||||||
|
@ -74,7 +72,7 @@ namespace RhinoReminds
|
||||||
xml.WriteElementString("NextId", nextId.ToString());
|
xml.WriteElementString("NextId", nextId.ToString());
|
||||||
|
|
||||||
xml.WriteStartElement("Reminders");
|
xml.WriteStartElement("Reminders");
|
||||||
foreach (Reminder nextReminder in Reminders.Values)
|
foreach (Reminder nextReminder in Reminders)
|
||||||
nextReminder.WriteToXml(xml);
|
nextReminder.WriteToXml(xml);
|
||||||
xml.WriteEndElement();
|
xml.WriteEndElement();
|
||||||
|
|
||||||
|
@ -89,15 +87,15 @@ namespace RhinoReminds
|
||||||
XmlDocument xml = new XmlDocument();
|
XmlDocument xml = new XmlDocument();
|
||||||
xml.Load(filepath);
|
xml.Load(filepath);
|
||||||
|
|
||||||
ReminderList result = new ReminderList();
|
ReminderList result = new ReminderList() {
|
||||||
result.nextId = int.Parse(xml.GetElementsByTagName("NextId")[0].InnerText);
|
nextId = int.Parse(xml.GetElementsByTagName("NextId")[0].InnerText)
|
||||||
foreach (XmlNode reminderXML in xml.GetElementsByTagName("Reminders")[0].ChildNodes)
|
};
|
||||||
{
|
|
||||||
|
foreach (XmlNode reminderXML in xml.GetElementsByTagName("Reminders")[0].ChildNodes) {
|
||||||
Reminder nextReminder = Reminder.FromXml(reminderXML);
|
Reminder nextReminder = Reminder.FromXml(reminderXML);
|
||||||
DateTime timeKey = nextReminder.Time;
|
while (result.Reminders.Contains(nextReminder))
|
||||||
while (result.Reminders.ContainsKey(timeKey))
|
nextReminder.TweakTime();
|
||||||
timeKey = timeKey.AddMilliseconds(1);
|
result.Reminders.Add(nextReminder);
|
||||||
result.Reminders.Add(timeKey, nextReminder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -43,19 +43,19 @@
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
<Reference Include="Microsoft.Recognizers.Definitions">
|
<Reference Include="Microsoft.Recognizers.Definitions">
|
||||||
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.3\lib\net462\Microsoft.Recognizers.Definitions.dll</HintPath>
|
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.4\lib\net462\Microsoft.Recognizers.Definitions.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Microsoft.Recognizers.Text">
|
<Reference Include="Microsoft.Recognizers.Text">
|
||||||
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.3\lib\net462\Microsoft.Recognizers.Text.dll</HintPath>
|
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.4\lib\net462\Microsoft.Recognizers.Text.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Microsoft.Recognizers.Text.Number">
|
<Reference Include="Microsoft.Recognizers.Text.Number">
|
||||||
<HintPath>..\packages\Microsoft.Recognizers.Text.Number.1.1.3\lib\net462\Microsoft.Recognizers.Text.Number.dll</HintPath>
|
<HintPath>..\packages\Microsoft.Recognizers.Text.Number.1.1.4\lib\net462\Microsoft.Recognizers.Text.Number.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Microsoft.Recognizers.Text.NumberWithUnit">
|
<Reference Include="Microsoft.Recognizers.Text.NumberWithUnit">
|
||||||
<HintPath>..\packages\Microsoft.Recognizers.Text.NumberWithUnit.1.1.3\lib\net462\Microsoft.Recognizers.Text.NumberWithUnit.dll</HintPath>
|
<HintPath>..\packages\Microsoft.Recognizers.Text.NumberWithUnit.1.1.4\lib\net462\Microsoft.Recognizers.Text.NumberWithUnit.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Microsoft.Recognizers.Text.DateTime">
|
<Reference Include="Microsoft.Recognizers.Text.DateTime">
|
||||||
<HintPath>..\packages\Microsoft.Recognizers.Text.DateTime.1.1.3\lib\net462\Microsoft.Recognizers.Text.DateTime.dll</HintPath>
|
<HintPath>..\packages\Microsoft.Recognizers.Text.DateTime.1.1.4\lib\net462\Microsoft.Recognizers.Text.DateTime.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -68,6 +68,8 @@
|
||||||
<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" />
|
||||||
|
<Compile Include="CompareReminders.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
|
|
42
RhinoReminds/SimpleXmppClient.cs
Normal file
42
RhinoReminds/SimpleXmppClient.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
<package id="Microsoft.Recognizers.Text" version="1.1.3" targetFramework="net47" />
|
<package id="Microsoft.Recognizers.Text" version="1.1.4" targetFramework="net47" />
|
||||||
<package id="Microsoft.Recognizers.Text.DateTime" version="1.1.3" targetFramework="net47" />
|
<package id="Microsoft.Recognizers.Text.DateTime" version="1.1.4" targetFramework="net47" />
|
||||||
<package id="Microsoft.Recognizers.Text.Number" version="1.1.3" targetFramework="net47" />
|
<package id="Microsoft.Recognizers.Text.Number" version="1.1.4" targetFramework="net47" />
|
||||||
<package id="Microsoft.Recognizers.Text.NumberWithUnit" version="1.1.3" targetFramework="net47" />
|
<package id="Microsoft.Recognizers.Text.NumberWithUnit" version="1.1.4" targetFramework="net47" />
|
||||||
<package id="S22.Xmpp" version="1.0.0.0" targetFramework="net47" />
|
<package id="S22.Xmpp" version="1.0.0.0" targetFramework="net47" />
|
||||||
<package id="System.Collections.Immutable" version="1.5.0" targetFramework="net47" />
|
<package id="System.Collections.Immutable" version="1.5.0" targetFramework="net47" />
|
||||||
<package id="System.ValueTuple" version="4.5.0" targetFramework="net47" />
|
<package id="System.ValueTuple" version="4.5.0" targetFramework="net47" />
|
||||||
|
|
|
@ -2,15 +2,53 @@
|
||||||
Description=RhinoReminds XMPP Bot
|
Description=RhinoReminds XMPP Bot
|
||||||
After=network.target prosody.service
|
After=network.target prosody.service
|
||||||
|
|
||||||
|
# No more than 5 crashes in 12 hours
|
||||||
|
StartLimitIntervalSec=43200
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=forking
|
||||||
# Another Type option: forking
|
PIDFile=/run/rhinoreminds/rhinoreminds.pid
|
||||||
User=rhinoreminds
|
# We change our own user
|
||||||
WorkingDirectory=/srv/rhinoreminds
|
User=root
|
||||||
ExecStart=/srv/rhinoreminds/start_service.sh
|
WorkingDirectory=/srv/kraggwapple
|
||||||
|
ExecStart=/srv/kraggwapple/start_service.sh
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
# Other Restart options: or always, on-abort, etc
|
# Other Restart options: or always, on-abort, etc
|
||||||
|
# Delay restarts by 60 seconds
|
||||||
|
RestartSec=60
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Kraggwapple XMPP Bot
|
||||||
|
After=network.target prosody.service
|
||||||
|
|
||||||
|
# No more than 5 crashes in 12 hours
|
||||||
|
StartLimitIntervalSec=43200
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=forking
|
||||||
|
PIDFile=/run/kraggwapple.pid
|
||||||
|
# We change our own user
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/srv/kraggwapple
|
||||||
|
ExecStart=/srv/kraggwapple/start_service.sh
|
||||||
|
Restart=on-failure
|
||||||
|
# Other Restart options: or always, on-abort, etc
|
||||||
|
# Delay restarts by 60 seconds
|
||||||
|
RestartSec=60
|
||||||
|
|
||||||
# If you want logs to be kept automatically in /var/log, uncomment the following & copy "rhinoreminds-rsyslog.conf" in the root of this repository to "/etc/rsyslog.d" - and then do `sudo systemctl restart rsyslog.d`
|
# If you want logs to be kept automatically in /var/log, uncomment the following & copy "rhinoreminds-rsyslog.conf" in the root of this repository to "/etc/rsyslog.d" - and then do `sudo systemctl restart rsyslog.d`
|
||||||
#StandardOutput=syslog
|
#StandardOutput=syslog
|
||||||
|
@ -19,3 +57,8 @@ Restart=on-failure
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
source .xmpp_credentials
|
cd data;
|
||||||
|
source .xmpp_credentials;
|
||||||
|
|
||||||
|
# Execute & disown
|
||||||
|
# We pass the environment variables explicitly here, as then we don't accidentally pass something private.
|
||||||
|
# Better to be safe than sorry - defence in depth!
|
||||||
export XMPP_JID;
|
export XMPP_JID;
|
||||||
export XMPP_PASSWORD;
|
export XMPP_PASSWORD;
|
||||||
|
|
||||||
exec /usr/bin/mono RhinoReminds.exe --domain starbeamrainbowlabs.com
|
# Create the pidfile directory
|
||||||
|
mkdir /run/rhinoreminds; chmod 0700 /run/rhinoreminds; chown rhinoreminds:rhinoreminds /run/rhinoreminds;
|
||||||
|
|
||||||
|
sudo -E -u rhinoreminds bash -c '/usr/bin/mono ../bin/RhinoReminds.exe --domain starbeamrainbowlabs.com --avatar avatar.png & echo "$!" >/run/rhinoreminds/rhinoreminds.pid; disown'
|
||||||
|
|
Loading…
Reference in a new issue