Compare commits
62 commits
Author | SHA1 | Date | |
---|---|---|---|
Starbeamrainbowlabs | c13dd77b5f | ||
Starbeamrainbowlabs | 3b072f3e97 | ||
Starbeamrainbowlabs | 3d4af3eb2f | ||
Starbeamrainbowlabs | ef8c90b4ad | ||
Starbeamrainbowlabs | b47c7e4cf7 | ||
Starbeamrainbowlabs | 23be94ff5e | ||
Starbeamrainbowlabs | eeea59eaff | ||
Starbeamrainbowlabs | dced63f02c | ||
Starbeamrainbowlabs | c9d2f1b939 | ||
Starbeamrainbowlabs | 141bccc03c | ||
Starbeamrainbowlabs | ab19b82d35 | ||
Starbeamrainbowlabs | 48c62f53e1 | ||
Starbeamrainbowlabs | 23fb6cf820 | ||
Starbeamrainbowlabs | b7f3f27042 | ||
Starbeamrainbowlabs | 64bb9550b3 | ||
Starbeamrainbowlabs | 23a9fa3ed7 | ||
Starbeamrainbowlabs | 49316836d9 | ||
Starbeamrainbowlabs | a256158f89 | ||
Starbeamrainbowlabs | e60dd12751 | ||
Starbeamrainbowlabs | 8099155455 | ||
Starbeamrainbowlabs | b32999f80e | ||
Starbeamrainbowlabs | c3908ec2c7 | ||
Starbeamrainbowlabs | 39420d70d5 | ||
Starbeamrainbowlabs | 44c809eb75 | ||
Starbeamrainbowlabs | 9b9dae00f8 | ||
Starbeamrainbowlabs | 29c307e297 | ||
Starbeamrainbowlabs | 6ec324ef7c | ||
Starbeamrainbowlabs | 9343ce875c | ||
Starbeamrainbowlabs | e23b2e0f3c | ||
Starbeamrainbowlabs | efffaa7465 | ||
Starbeamrainbowlabs | de1cc6bb51 | ||
Starbeamrainbowlabs | 83c1246fab | ||
Starbeamrainbowlabs | fd74e5f3a7 | ||
Starbeamrainbowlabs | 7a5a43f9a2 | ||
Starbeamrainbowlabs | 22c2212952 | ||
Starbeamrainbowlabs | 950d12f08b | ||
Starbeamrainbowlabs | 48cc42ef2a | ||
Starbeamrainbowlabs | 1360eb9c6f | ||
Starbeamrainbowlabs | 1f88789309 | ||
Starbeamrainbowlabs | 09669d9acf | ||
Starbeamrainbowlabs | a4d996fb3d | ||
Starbeamrainbowlabs | b5deb649dc | ||
Starbeamrainbowlabs | 6d400b7c3a | ||
Starbeamrainbowlabs | 46f55a086b | ||
Starbeamrainbowlabs | 20f5329b9c | ||
Starbeamrainbowlabs | 7ae561a050 | ||
Starbeamrainbowlabs | 99101cd519 | ||
Starbeamrainbowlabs | fef5dcbc79 | ||
Starbeamrainbowlabs | c5e54864bc | ||
Starbeamrainbowlabs | 6f9ff7eef0 | ||
Starbeamrainbowlabs | 517e3dcafc | ||
Starbeamrainbowlabs | 97a6d95902 | ||
Starbeamrainbowlabs | 1314b3ab75 | ||
Starbeamrainbowlabs | 7cbf1ef79c | ||
Starbeamrainbowlabs | 430b5082dd | ||
Starbeamrainbowlabs | 579078edf0 | ||
Starbeamrainbowlabs | adfb482072 | ||
Starbeamrainbowlabs | 4174ec85c6 | ||
Starbeamrainbowlabs | 505e500634 | ||
Starbeamrainbowlabs | b5df7ab94e | ||
Starbeamrainbowlabs | dd96754126 | ||
Starbeamrainbowlabs | e58f56b06d |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
git-hash.txt
|
||||
|
||||
*.tar.gz
|
||||
*.backup
|
||||
# Created by https://www.gitignore.io/api/monodevelop,visualstudio,csharp,git
|
||||
# Edit at https://www.gitignore.io/?templates=monodevelop,visualstudio,csharp,git
|
||||
|
||||
|
|
27
README.md
27
README.md
|
@ -2,7 +2,15 @@
|
|||
|
||||
> An XMPP reminder bot written in C#.
|
||||
|
||||
I've blogged about this project here: [RhinoReminds: An XMPP reminder bot for my convenience](https://starbeamrainbowlabs.com/blog/article.php?article=posts/328-RhinoReminds.html)
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
|
@ -32,6 +40,14 @@ mono [--debug] RhinoReminds.exe [--help]
|
|||
RhinoReminds.exe [--help]
|
||||
```
|
||||
|
||||
### Setting up RhinoReminds as a system service
|
||||
Some helpful template files are located in this repository to aid in setting _RhinoReminds_ up as a system service.
|
||||
|
||||
- [`start_service.sh`](https://git.starbeamrainbowlabs.com/sbrl/RhinoReminds/src/branch/master/start_service.sh) Contains a script that reads in CLI variables from a file, sets up a PID file directory with the appropriate permissions, and then executes _RhinoReminds_ as an unprivileged user.
|
||||
- [`rhinoreminds.service`](https://git.starbeamrainbowlabs.com/sbrl/RhinoReminds/src/branch/master/rhinoreminds.service) Contains a systemd service file compatible with `start_service.sh`
|
||||
- [`rhinoreminds-rsyslog.conf`](https://git.starbeamrainbowlabs.com/sbrl/RhinoReminds/src/branch/master/rhinoreminds-rsyslog.conf) Contains an Rsyslog definition file compatible with the systemd service file defined above. When put in `/etc/rsyslog.d` (don't forget to restart the `rsyslog` service!), it will write and auto-rotate log files of the standard output and standard error of the main _RhinoReminds_ process to a subfolder fo `/var/log` automatically.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
|
@ -61,6 +77,17 @@ 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
|
||||
- [Microsoft.Text.Recognizers Samples](https://github.com/Microsoft/Recognizers-Text/tree/master/.NET/Samples)
|
||||
- [S22.Xmpp API Documentation](https://smiley22.github.io/S22.Xmpp/Documentation/)
|
||||
|
|
|
@ -9,7 +9,6 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using RhinoReminds.Utilities;
|
||||
using S22.Xmpp;
|
||||
using S22.Xmpp.Client;
|
||||
using S22.Xmpp.Im;
|
||||
using SBRL.Geometry;
|
||||
|
||||
|
@ -24,35 +23,35 @@ 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<string> AllowedDomains = new List<string>();
|
||||
|
||||
public string ReminderFilePath { get; set; } = "./reminders.xml";
|
||||
private ReminderList reminderList = new ReminderList();
|
||||
private CancellationTokenSource reminderWatcherReset;
|
||||
private CancellationToken reminderWatcherResetToken => reminderWatcherReset.Token;
|
||||
|
||||
|
||||
private XmppClient client;
|
||||
private SimpleXmppClient client;
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private int nextBackoffDelay = 1;
|
||||
private int defaultBackoffDelay = 1;
|
||||
private float backoffDelayMultiplier = 2;
|
||||
private readonly int defaultBackoffDelay = 1;
|
||||
private readonly float backoffDelayMultiplier = 2;
|
||||
/// <summary>
|
||||
/// If a connection attempt doesn't succeed in this number of seconds,
|
||||
/// give up and try again later.
|
||||
/// </summary>
|
||||
private int giveUpTimeout = 30;
|
||||
private readonly int giveUpTimeout = 30;
|
||||
|
||||
|
||||
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<bool> 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)
|
||||
{
|
||||
|
@ -94,17 +115,25 @@ namespace RhinoReminds
|
|||
}
|
||||
|
||||
Console.WriteLine($"[Rhino/Setup] Connected as {Jid}");
|
||||
OnConnected(this, new OnConnectedEventArgs());
|
||||
OnConnected?.Invoke(this, new OnConnectedEventArgs());
|
||||
|
||||
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,20 @@ 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, "**Version:** Show the program version I am currently running");
|
||||
client.SendChatReply(message, "\nExample: 'Remind me to feed the cat tomorrow at 6pm'");
|
||||
break;
|
||||
|
||||
case "version":
|
||||
client.SendChatReply(message, $"I'm currently running {Program.Version}.");
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
|
@ -201,19 +226,21 @@ 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) {
|
||||
// Ensure that the reminder thread picks up the changes
|
||||
interruptReminderWatcher();
|
||||
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;
|
||||
|
||||
case "list":
|
||||
case "show":
|
||||
// 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()
|
||||
);
|
||||
StringBuilder listMessage = new StringBuilder("I've got the following reminders on my list:\n");
|
||||
|
@ -222,7 +249,7 @@ namespace RhinoReminds
|
|||
}
|
||||
listMessage.AppendLine();
|
||||
listMessage.AppendLine($"({userReminderList.Count()} total)");
|
||||
sendChatReply(message, listMessage.ToString());
|
||||
client.SendChatReply(message, listMessage.ToString());
|
||||
|
||||
break;
|
||||
|
||||
|
@ -231,8 +258,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(
|
||||
|
@ -245,136 +272,144 @@ namespace RhinoReminds
|
|||
@"^remind\s+(?:me\s+)?",
|
||||
@"^me\s+",
|
||||
@"^on\s+",
|
||||
@"my",
|
||||
@"you",
|
||||
@"your",
|
||||
@"\byou\b",
|
||||
@"\byour\b",
|
||||
@"\bmy\b",
|
||||
@"&" // Ampersands cause a crash when sending!
|
||||
}, new string[] {
|
||||
" ",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"your",
|
||||
@"me",
|
||||
@"my",
|
||||
"your",
|
||||
@"and"
|
||||
}, RegexOptions.IgnoreCase).Trim();
|
||||
|
||||
sendChatReply(message, $"Ok! I'll remind you {reminder} at {dateTime}.");
|
||||
|
||||
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);
|
||||
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
|
||||
/// <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
|
||||
|
||||
#region Reminder Listening
|
||||
|
||||
private void interruptReminderWatcher()
|
||||
{
|
||||
CancellationTokenSource oldToken = reminderWatcherReset;
|
||||
reminderWatcherReset = new CancellationTokenSource();
|
||||
oldToken?.Cancel();
|
||||
}
|
||||
|
||||
private async Task watchForReminders()
|
||||
{
|
||||
CancellationTokenSource cancellationSource = new CancellationTokenSource();
|
||||
CancellationToken cancellationToken = cancellationSource.Token;
|
||||
interruptReminderWatcher();
|
||||
Reminder nextReminder = reminderList.GetNextReminder();
|
||||
reminderList.OnReminderListUpdate += (object sender, Reminder newReminder) => {
|
||||
|
||||
// ----- Events -----
|
||||
// This will run on the firing thread, not on this thread
|
||||
reminderList.OnReminderListUpdate += (object sender, ReminderListUpdateEventArgs eventArgs) => {
|
||||
Reminder newNextReminder = reminderList.GetNextReminder();
|
||||
//Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing.");
|
||||
//Console.WriteLine($"[Rhino/Reminderd/Canceller] {nextReminder} / {newNextReminder}");
|
||||
Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing.");
|
||||
Console.WriteLine($"[Rhino/Reminderd/Canceller] {nextReminder} / {newNextReminder}");
|
||||
if (nextReminder != newNextReminder) {
|
||||
//Console.WriteLine($"[Rhino/Reminderd/Canceller] Cancelling");
|
||||
Console.WriteLine($"[Rhino/Reminderd/Canceller] Cancelling");
|
||||
nextReminder = newNextReminder;
|
||||
cancellationSource.Cancel();
|
||||
interruptReminderWatcher();
|
||||
}
|
||||
};
|
||||
// ------------------
|
||||
|
||||
while (true)
|
||||
{
|
||||
TimeSpan nextWaitingTime;
|
||||
while (true) {
|
||||
nextReminder = reminderList.GetNextReminder();
|
||||
// Wait for the next reminder
|
||||
TimeSpan nextWaitingTime = new TimeSpan();
|
||||
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);
|
||||
if (DateTime.Now < nextReminder.Time)
|
||||
{
|
||||
Console.WriteLine($"[Rhino/Reminderd] Next reminder: {nextReminder}");
|
||||
Console.WriteLine($"[Rhino/Reminderd] Sleeping for {nextWaitingTime}");
|
||||
int nextWaitingTimeMs = (int)nextWaitingTime.TotalMilliseconds;
|
||||
if (nextWaitingTimeMs < 0) // if it overflows, sort it out
|
||||
nextWaitingTimeMs = int.MaxValue;
|
||||
await Task.Delay(nextWaitingTimeMs, reminderWatcherResetToken);
|
||||
}
|
||||
} 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;
|
||||
else {
|
||||
Console.WriteLine("[Rhino/Reminderd] Sleeping until interrupted");
|
||||
await Task.Delay(Timeout.Infinite, reminderWatcherResetToken);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested) {
|
||||
if (reminderWatcherResetToken.IsCancellationRequested) {
|
||||
Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating (but no exception thrown)");
|
||||
cancellationSource = new CancellationTokenSource();
|
||||
cancellationToken = cancellationSource.Token;
|
||||
|
||||
continue;
|
||||
}
|
||||
if (nextReminder.Time > DateTime.Now.AddSeconds(-1)) {
|
||||
Console.WriteLine("[Rhino/Reminderd] Didn't sleep for long enough, going back to bed *yawn*");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Rhino/Reminderd] Sending notification {nextReminder}");
|
||||
Jid targetJid = nextReminder.Jid; // Apparently nextReminder is null after the sendAndDeleteReminder() call - very odd!
|
||||
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) {
|
||||
sendChatMessage(
|
||||
nextReminder.Jid,
|
||||
if (nextWaitingTime.TotalMilliseconds < -1 * 2000) {
|
||||
client.SendChatMessage(
|
||||
targetJid,
|
||||
"(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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
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 x.Time.CompareTo(y.Time);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using RhinoReminds.Utilities;
|
||||
using S22.Xmpp;
|
||||
using S22.Xmpp.Client;
|
||||
using S22.Xmpp.Im;
|
||||
using SBRL.Utilities;
|
||||
|
||||
namespace RhinoReminds
|
||||
{
|
||||
|
@ -15,12 +19,21 @@ namespace RhinoReminds
|
|||
public string Jid = null;
|
||||
|
||||
public string AvatarFilepath = string.Empty;
|
||||
public string PidFile = null;
|
||||
|
||||
public bool ExitOnModify = false;
|
||||
|
||||
public string Password = null;
|
||||
}
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static string Version {
|
||||
get {
|
||||
return $"v{Assembly.GetExecutingAssembly().GetName().Version}" +
|
||||
$"-{EmbeddedFiles.ReadAllText("RhinoReminds.git-hash.txt").Substring(0, 7)}";
|
||||
}
|
||||
}
|
||||
private static ProgramSettings settings = new ProgramSettings();
|
||||
|
||||
public static int Main(string[] args)
|
||||
|
@ -36,7 +49,7 @@ namespace RhinoReminds
|
|||
switch (args[i]) {
|
||||
case "-h":
|
||||
case "--help":
|
||||
Console.WriteLine("--- RhinoReminds ---");
|
||||
Console.WriteLine($"--- RhinoReminds {Version} ---");
|
||||
Console.WriteLine("> An XMPP reminder bot");
|
||||
Console.WriteLine(" By Starbeamrainbowlabs");
|
||||
Console.WriteLine();
|
||||
|
@ -44,10 +57,12 @@ namespace RhinoReminds
|
|||
Console.WriteLine(" mono RhinoReminds.exe {options}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Options:");
|
||||
Console.WriteLine(" -h --help Show this message");
|
||||
Console.WriteLine($" -f --file Specify where to save reminders (default: {settings.Filepath})");
|
||||
Console.WriteLine(" -h --help Show this message");
|
||||
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(" --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(" --auto-exit Watch for changes to the executable on disk and automatically quit with an exit code of 0 if a modification is detected. Useful for integration with a service manager and continuous deployment.");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Environment Variables:");
|
||||
Console.WriteLine(" XMPP_JID The JID to login to");
|
||||
|
@ -67,33 +82,96 @@ namespace RhinoReminds
|
|||
case "--avatar":
|
||||
settings.AvatarFilepath = args[++i];
|
||||
break;
|
||||
|
||||
case "--pidfile":
|
||||
settings.PidFile = args[++i];
|
||||
break;
|
||||
|
||||
case "--auto-exit":
|
||||
settings.ExitOnModify = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
Console.Error.WriteLine($"Error: Unknown argument '{args[i]}'.");
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
settings.Jid = Environment.GetEnvironmentVariable("XMPP_JID");
|
||||
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;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
Console.WriteLine("************************************");
|
||||
Console.WriteLine("***** RhinoReminds is starting *****");
|
||||
Console.WriteLine("************************************");
|
||||
Console.WriteLine($"[Program] Running {Version}");
|
||||
|
||||
if (settings.ExitOnModify) {
|
||||
ExitWatcher.EnableExitOnModify();
|
||||
}
|
||||
|
||||
ClientListener client = new ClientListener(settings.Jid, settings.Password) {
|
||||
ReminderFilePath = settings.Filepath
|
||||
};
|
||||
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
|
||||
client.Start().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.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Xml;
|
||||
using S22.Xmpp;
|
||||
|
@ -6,29 +7,59 @@ using S22.Xmpp;
|
|||
namespace RhinoReminds
|
||||
{
|
||||
|
||||
public class Reminder
|
||||
public class Reminder : IEquatable<Reminder>
|
||||
{
|
||||
public int Id { get; }
|
||||
public string Jid { get; }
|
||||
public Jid JidObj => new Jid(Jid);
|
||||
public DateTime Time { get; }
|
||||
public string Jid => JidObj.ToString();
|
||||
public Jid JidObj { get; }
|
||||
public DateTime Time { get; private set; }
|
||||
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;
|
||||
Jid = inJid;
|
||||
JidObj = inJid.GetBareJid();
|
||||
Time = inTime;
|
||||
Message = inMessage;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[Reminder Id={Id}, Jid={Jid}, Time={Time}, Message={Message}";
|
||||
public void TweakTime() {
|
||||
Time = Time.AddMilliseconds(1);
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
|
@ -66,7 +97,8 @@ namespace RhinoReminds
|
|||
return new Reminder(id, jid, dateTime, message);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using S22.Xmpp;
|
||||
|
||||
namespace RhinoReminds
|
||||
{
|
||||
public delegate void OnReminderListUpdateHandler(object sender, Reminder newReminder);
|
||||
public class ReminderListUpdateEventArgs : EventArgs { }
|
||||
public delegate void OnReminderListUpdateHandler(object sender, ReminderListUpdateEventArgs newReminder);
|
||||
|
||||
public class ReminderList
|
||||
{
|
||||
private int nextId = 0;
|
||||
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;
|
||||
|
||||
|
@ -29,29 +29,30 @@ namespace RhinoReminds
|
|||
{
|
||||
Reminder result = new Reminder(nextId++, $"{inJid.Node}@{inJid.Domain}", time, message);
|
||||
Console.WriteLine($"[Rhino/ReminderList] Created reminder {result}");
|
||||
while (Reminders.ContainsKey(time))
|
||||
time = time.AddMilliseconds(1);
|
||||
Reminders.Add(time, result);
|
||||
OnReminderListUpdate(this, result);
|
||||
while (Reminders.Contains(result))
|
||||
result.TweakTime();
|
||||
|
||||
Reminders.Add(result);
|
||||
OnReminderListUpdate(this, new ReminderListUpdateEventArgs());
|
||||
return result;
|
||||
}
|
||||
|
||||
public Reminder GetNextReminder()
|
||||
{
|
||||
public Reminder GetNextReminder() {
|
||||
if (Reminders.Count == 0)
|
||||
return null;
|
||||
|
||||
return Reminders.Values[0];
|
||||
return Reminders.Min;
|
||||
}
|
||||
|
||||
public Reminder GetById(int targetId)
|
||||
{
|
||||
return Reminders.First((KeyValuePair<DateTime, Reminder> nextPair) => nextPair.Value.Id == targetId).Value;
|
||||
public Reminder GetById(int targetId) {
|
||||
return Reminders.First((Reminder nextReminder) => nextReminder.Id == targetId);
|
||||
}
|
||||
|
||||
public void DeleteReminder(Reminder nextReminder)
|
||||
{
|
||||
Reminders.Remove(nextReminder.Time);
|
||||
public void DeleteReminder(Reminder nextReminder) {
|
||||
if (!Reminders.Remove(nextReminder))
|
||||
throw new ApplicationException($"Error: Failed to remove the reminder {nextReminder} from the list!");
|
||||
|
||||
OnReminderListUpdate(this, new ReminderListUpdateEventArgs());
|
||||
}
|
||||
|
||||
|
||||
|
@ -62,7 +63,7 @@ namespace RhinoReminds
|
|||
// 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
|
||||
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(
|
||||
filename,
|
||||
new XmlWriterSettings() { Indent = true }
|
||||
|
@ -74,7 +75,7 @@ namespace RhinoReminds
|
|||
xml.WriteElementString("NextId", nextId.ToString());
|
||||
|
||||
xml.WriteStartElement("Reminders");
|
||||
foreach (Reminder nextReminder in Reminders.Values)
|
||||
foreach (Reminder nextReminder in Reminders)
|
||||
nextReminder.WriteToXml(xml);
|
||||
xml.WriteEndElement();
|
||||
|
||||
|
@ -89,20 +90,28 @@ namespace RhinoReminds
|
|||
XmlDocument xml = new XmlDocument();
|
||||
xml.Load(filepath);
|
||||
|
||||
ReminderList result = new ReminderList();
|
||||
result.nextId = int.Parse(xml.GetElementsByTagName("NextId")[0].InnerText);
|
||||
foreach (XmlNode reminderXML in xml.GetElementsByTagName("Reminders")[0].ChildNodes)
|
||||
{
|
||||
ReminderList result = new ReminderList() {
|
||||
nextId = int.Parse(xml.GetElementsByTagName("NextId")[0].InnerText)
|
||||
};
|
||||
|
||||
foreach (XmlNode reminderXML in xml.GetElementsByTagName("Reminders")[0].ChildNodes) {
|
||||
Reminder nextReminder = Reminder.FromXml(reminderXML);
|
||||
DateTime timeKey = nextReminder.Time;
|
||||
while (result.Reminders.ContainsKey(timeKey))
|
||||
timeKey = timeKey.AddMilliseconds(1);
|
||||
result.Reminders.Add(timeKey, nextReminder);
|
||||
while (result.Reminders.Contains(nextReminder))
|
||||
nextReminder.TweakTime();
|
||||
result.Reminders.Add(nextReminder);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string result = "Reminder list:";
|
||||
foreach (Reminder nextReminder in Reminders)
|
||||
Console.WriteLine($" - {nextReminder}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<ExternalConsole>true</ExternalConsole>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
|
||||
<Optimize>true</Optimize>
|
||||
|
@ -27,7 +26,6 @@
|
|||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<ExternalConsole>true</ExternalConsole>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
|
@ -43,19 +41,19 @@
|
|||
</Reference>
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Microsoft.Recognizers.Definitions">
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.4\lib\net462\Microsoft.Recognizers.Definitions.dll</HintPath>
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.6\lib\net462\Microsoft.Recognizers.Definitions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Recognizers.Text">
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.4\lib\net462\Microsoft.Recognizers.Text.dll</HintPath>
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.1.1.6\lib\net462\Microsoft.Recognizers.Text.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Recognizers.Text.Number">
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.Number.1.1.4\lib\net462\Microsoft.Recognizers.Text.Number.dll</HintPath>
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.Number.1.1.6\lib\net462\Microsoft.Recognizers.Text.Number.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Recognizers.Text.NumberWithUnit">
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.NumberWithUnit.1.1.4\lib\net462\Microsoft.Recognizers.Text.NumberWithUnit.dll</HintPath>
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.NumberWithUnit.1.1.6\lib\net462\Microsoft.Recognizers.Text.NumberWithUnit.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Recognizers.Text.DateTime">
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.DateTime.1.1.4\lib\net462\Microsoft.Recognizers.Text.DateTime.dll</HintPath>
|
||||
<HintPath>..\packages\Microsoft.Recognizers.Text.DateTime.1.1.6\lib\net462\Microsoft.Recognizers.Text.DateTime.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -68,6 +66,10 @@
|
|||
<Compile Include="Exceptions.cs" />
|
||||
<Compile Include="Utilities\Range.cs" />
|
||||
<Compile Include="Utilities\TextHelpers.cs" />
|
||||
<Compile Include="SimpleXmppClient.cs" />
|
||||
<Compile Include="CompareReminders.cs" />
|
||||
<Compile Include="Utilities\EmbeddedFiles.cs" />
|
||||
<Compile Include="Utilities\ExitWatcher.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
@ -76,4 +78,11 @@
|
|||
<Folder Include="Utilities\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
|
||||
<Target Name="BeforeBuild" BeforeTargets="Build">
|
||||
<Exec Command="git rev-parse HEAD >git-hash.txt" WorkingDirectory="$(ProjectDir)" IgnoreExitCode="true" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="git-hash.txt" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
287
RhinoReminds/Utilities/EmbeddedFiles.cs
Normal file
287
RhinoReminds/Utilities/EmbeddedFiles.cs
Normal file
|
@ -0,0 +1,287 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SBRL.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of static methods for manipulating embedded resources.
|
||||
/// </summary>
|
||||
/// <description>
|
||||
/// From https://gist.github.com/sbrl/aabfcfe87396b8c05d3263887b807d23. You may have seen
|
||||
/// this in several other ACWs I've done. Proof I wrote this is available upon request,
|
||||
/// of course.
|
||||
///
|
||||
/// v0.6.2, by Starbeamrainbowlabs <feedback@starbeamrainbowlabs.com>
|
||||
/// Last updated 28th November 2018.
|
||||
/// Licensed under MPL-2.0.
|
||||
///
|
||||
/// Changelog:
|
||||
/// v0.1 (25th July 2016):
|
||||
/// - Initial release.
|
||||
/// v0.2 (8th August 2016):
|
||||
/// - Changed namespace.
|
||||
/// v0.3 (21st January 2017):
|
||||
/// - Added GetRawReader().
|
||||
/// v0.4 (8th April 2017):
|
||||
/// - Removed unnecessary using statement.
|
||||
/// v0.5 (3rd September 2017):
|
||||
/// - Changed namespace
|
||||
/// v0.6 (12th October 2018):
|
||||
/// - Fixed assembly / calling assembly bugs
|
||||
/// v0.6.1 (17th october 2018):
|
||||
/// - Fix crash in ReadAllText(filename)
|
||||
/// v0.6.2 (28th November 2018):
|
||||
/// - Fix assembly targeting bug in ReadAllBytesAsync()
|
||||
/// </description>
|
||||
public static class EmbeddedFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// An array of the filenames of all the resources embedded in the target assembly.
|
||||
/// </summary>
|
||||
/// <param name="targetAssembly">The target assembly to extract a resource list for.</param>
|
||||
/// <value>The resource list.</value>
|
||||
public static string[] ResourceList(Assembly targetAssembly)
|
||||
{
|
||||
return targetAssembly.GetManifestResourceNames();
|
||||
}
|
||||
/// <summary>
|
||||
/// An array of the filenames of all the resources embedded in the calling assembly.
|
||||
/// </summary>
|
||||
/// <value>The resource list.</value>
|
||||
public static string[] ResourceList()
|
||||
{
|
||||
return ResourceList(Assembly.GetCallingAssembly());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of resources embedded in the calling assembly as a string.
|
||||
/// </summary>
|
||||
public static string GetResourceListText()
|
||||
{
|
||||
return GetResourceListText(Assembly.GetCallingAssembly());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of resources embedded in the target assembly as a string.
|
||||
/// </summary>
|
||||
/// <param name="targetAssembly">The target assembly to extract a resource list from.</param>
|
||||
public static string GetResourceListText(Assembly targetAssembly)
|
||||
{
|
||||
StringWriter result = new StringWriter();
|
||||
result.WriteLine("Files embedded in {0}:", targetAssembly.GetName().Name);
|
||||
foreach (string filename in ResourceList(targetAssembly))
|
||||
result.WriteLine(" - {0}", filename);
|
||||
return result.ToString();
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes a list of resources embedded in the calling assembly to the standard output.
|
||||
/// </summary>
|
||||
public static void WriteResourceList()
|
||||
{
|
||||
Console.WriteLine(GetResourceListText(Assembly.GetCallingAssembly()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a StreamReader attached to the specified embedded resource.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename of the embedded resource to get a StreamReader of.</param>
|
||||
/// <returns>A StreamReader attached to the specified embedded resource.</returns>
|
||||
public static StreamReader GetReader(string filename)
|
||||
{
|
||||
return new StreamReader(GetRawReader(filename));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a raw Stream that's attached to the specified embedded resource
|
||||
/// in the calling assembly.
|
||||
/// Useful when you want to copy an embedded resource to some other stream.
|
||||
/// </summary>
|
||||
/// <param name="filename">The path to the embedded resource.</param>
|
||||
/// <returns>A raw Stream object attached to the specified file.</returns>
|
||||
public static Stream GetRawReader(string filename)
|
||||
{
|
||||
return GetRawReader(Assembly.GetCallingAssembly(), filename);
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets a raw Stream that's attached to the specified embedded resource
|
||||
/// in the specified assembly.
|
||||
/// Useful when you want to copy an embedded resource to some other stream.
|
||||
/// </summary>
|
||||
/// <param name="targetAssembly">The assembly to search for the filename in.</param>
|
||||
/// <param name="filename">The path to the embedded resource.</param>
|
||||
/// <returns>A raw Stream object attached to the specified file.</returns>
|
||||
public static Stream GetRawReader(Assembly targetAssembly, string filename)
|
||||
{
|
||||
return targetAssembly.GetManifestResourceStream(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified embedded resource's content as a byte array.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename of the embedded resource to get conteent of.</param>
|
||||
/// <returns>The specified embedded resource's content as a byte array.</returns>
|
||||
public static byte[] ReadAllBytes(string filename)
|
||||
{
|
||||
// Referencing the Result property will block until the async method completes
|
||||
return ReadAllBytesAsync(filename).Result;
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets the content of the resource that's embedded in the specified
|
||||
/// assembly as a byte array asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="targetAssembly">The assembly to search for the file in.</param>
|
||||
/// <param name="filename">The filename of the embedded resource to get content of.</param>
|
||||
/// <returns>The specified embedded resource's content as a byte array.</returns>
|
||||
public static async Task<byte[]> ReadAllBytesAsync(Assembly targetAssembly, string filename)
|
||||
{
|
||||
using (Stream resourceStream = targetAssembly.GetManifestResourceStream(filename))
|
||||
using (MemoryStream temp = new MemoryStream())
|
||||
{
|
||||
await resourceStream.CopyToAsync(temp);
|
||||
return temp.ToArray();
|
||||
}
|
||||
}
|
||||
public static async Task<byte[]> ReadAllBytesAsync(string filename)
|
||||
{
|
||||
return await ReadAllBytesAsync(Assembly.GetCallingAssembly(), filename);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the text stored in the resource that's embedded in the
|
||||
/// calling assembly.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename to fetch the content of.</param>
|
||||
/// <returns>All the text stored in the specified embedded resource.</returns>
|
||||
public static string ReadAllText(string filename)
|
||||
{
|
||||
return ReadAllTextAsync(Assembly.GetCallingAssembly(), filename).Result;
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets all the text stored in the resource that's embedded in the
|
||||
/// specified assembly.
|
||||
/// </summary>
|
||||
/// <param name="targetAssembly">The assembly from in which to look for the target embedded resource.</param>
|
||||
/// <param name="filename">The filename to fetch the content of.</param>
|
||||
/// <returns>All the text stored in the specified embedded resource.</returns>
|
||||
public static string ReadAllText(Assembly targetAssembly, string filename)
|
||||
{
|
||||
return ReadAllTextAsync(targetAssembly, filename).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the text stored in the resource that's embedded in the
|
||||
/// specified assembly asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename to fetch the content of.</param>
|
||||
/// <returns>All the text stored in the specified embedded resource.</returns>
|
||||
public static async Task<string> ReadAllTextAsync(Assembly targetAssembly, string filename)
|
||||
{
|
||||
using (StreamReader resourceReader = new StreamReader(targetAssembly.GetManifestResourceStream(filename)))
|
||||
{
|
||||
return await resourceReader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets all the text stored in the resource that's embedded in the
|
||||
/// calling assembly asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename to fetch the content of.</param>
|
||||
/// <returns>All the text stored in the specified embedded resource.</returns>
|
||||
public static async Task<string> ReadAllTextAsync(string filename)
|
||||
{
|
||||
return await ReadAllTextAsync(Assembly.GetCallingAssembly(), filename);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the lines of text in the embedded resource that's
|
||||
/// embedded in the calling assembly.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename of the embedded resource to enumerate.</param>
|
||||
/// <returns>An IEnumerator that enumerates the specified embedded resource.</returns>
|
||||
public static IEnumerable<string> EnumerateLines(string filename)
|
||||
{
|
||||
return EnumerateLines(Assembly.GetCallingAssembly(), filename);
|
||||
}
|
||||
/// <summary>
|
||||
/// Enumerates the lines of text in the embedded resource that's
|
||||
/// embedded in the specified assembly.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename of the embedded resource to enumerate.</param>
|
||||
/// <returns>An IEnumerator that enumerates the specified embedded resource.</returns>
|
||||
public static IEnumerable<string> EnumerateLines(Assembly targetAssembly, string filename)
|
||||
{
|
||||
foreach (Task<string> nextLine in EnumerateLinesAsync(targetAssembly, filename))
|
||||
yield return nextLine.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the lines of text in the resource that's embedded in the
|
||||
/// specified assembly asynchronously.
|
||||
/// Each successive call returns a task that, when complete, returns
|
||||
/// the next line of text stored in the embedded resource.
|
||||
/// </summary>
|
||||
/// <param name="targetAssembly">The target assembly in which to look for the embedded resource.</param>
|
||||
/// <param name="filename">The filename of the embedded resource to enumerate.</param>
|
||||
/// <returns>An IEnumerator that enumerates the specified embedded resource.</returns>
|
||||
public static IEnumerable<Task<string>> EnumerateLinesAsync(Assembly targetAssembly, string filename)
|
||||
{
|
||||
using (StreamReader resourceReader = new StreamReader(targetAssembly.GetManifestResourceStream(filename)))
|
||||
{
|
||||
while (!resourceReader.EndOfStream)
|
||||
yield return resourceReader.ReadLineAsync();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Enumerates the lines of text in the resource that's embedded in the
|
||||
/// calling assembly asynchronously.
|
||||
/// Each successive call returns a task that, when complete, returns
|
||||
/// the next line of text stored in the embedded resource.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename of the embedded resource to enumerate.</param>
|
||||
/// <returns>An IEnumerator that enumerates the specified embedded resource.</returns>
|
||||
public static IEnumerable<Task<string>> EnumerateLinesAsync(string filename)
|
||||
{
|
||||
return EnumerateLinesAsync(Assembly.GetCallingAssembly(), filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the lines of text in the specified embedded resource.
|
||||
/// You might find EnumerateLines(string filename) more useful depending on your situation.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename to obtain the lines of text from.</param>
|
||||
/// <returns>A list of lines in the specified embedded resource.</returns>
|
||||
public static List<string> GetAllLines(string filename)
|
||||
{
|
||||
// Referencing the Result property will block until the async method completes
|
||||
return GetAllLinesAsync(filename).Result;
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets all the lines of text in the resource that's embedded in the
|
||||
/// calling assembly asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename to obtain the lines of text from.</param>
|
||||
/// <returns>A list of lines in the specified embedded resource.</returns>
|
||||
public static async Task<List<string>> GetAllLinesAsync(string filename)
|
||||
{
|
||||
return await GetAllLinesAsync(Assembly.GetCallingAssembly(), filename);
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets all the lines of text in the resource that's embedded in the
|
||||
/// specified assembly asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename to obtain the lines of text from.</param>
|
||||
/// <returns>A list of lines in the specified embedded resource.</returns>
|
||||
public static async Task<List<string>> GetAllLinesAsync(Assembly targetAssembly, string filename)
|
||||
{
|
||||
List<string> result = new List<string>();
|
||||
foreach (Task<string> nextLine in EnumerateLinesAsync(targetAssembly, filename))
|
||||
result.Add(await nextLine);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
39
RhinoReminds/Utilities/ExitWatcher.cs
Normal file
39
RhinoReminds/Utilities/ExitWatcher.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RhinoReminds.Utilities
|
||||
{
|
||||
public static class ExitWatcher
|
||||
{
|
||||
|
||||
private static int restartDelayMs = 5 * 1000;
|
||||
private static string EntryExecutablePath => Assembly.GetEntryAssembly().Location;
|
||||
|
||||
private static FileSystemWatcher watcher;
|
||||
private static bool exitSequenceStarted = false;
|
||||
|
||||
public static void EnableExitOnModify() {
|
||||
Console.WriteLine("[Program/ExitWatcher] Enabling auto-exit on modification.");
|
||||
watcher = new FileSystemWatcher(Path.GetDirectoryName(EntryExecutablePath), "*.exe");
|
||||
watcher.Changed += (object sender, FileSystemEventArgs e) => doExit();
|
||||
|
||||
watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
private static void doExit() {
|
||||
// Don't bother if we're already exiting on a different thread or something else weird
|
||||
if (exitSequenceStarted) return;
|
||||
|
||||
exitSequenceStarted = true;
|
||||
watcher.EnableRaisingEvents = false; // We're only interested in the first event raised
|
||||
|
||||
Console.WriteLine($"[Program/ExitWatcher] Modification detected, waiting {restartDelayMs}ms...");
|
||||
Thread.Sleep(restartDelayMs);
|
||||
Console.WriteLine($"[Program/ExitWatcher] Wait complete, exiting with code 0");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Recognizers.Text" version="1.1.4" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text.DateTime" version="1.1.4" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text.Number" version="1.1.4" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text.NumberWithUnit" version="1.1.4" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text" version="1.1.6" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text.DateTime" version="1.1.6" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text.Number" version="1.1.6" targetFramework="net47" />
|
||||
<package id="Microsoft.Recognizers.Text.NumberWithUnit" version="1.1.6" 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.ValueTuple" version="4.5.0" targetFramework="net47" />
|
||||
|
|
145
build
145
build
|
@ -18,10 +18,18 @@ lantern_path="./lantern-build-engine";
|
|||
# Put any custom settings here.
|
||||
build_output_folder="./dist";
|
||||
|
||||
deploy_ssh_user="ci";
|
||||
deploy_ssh_host="starbeamrainbowlabs.com";
|
||||
deploy_ssh_port="2403";
|
||||
deploy_target_dir="RhinoReminds";
|
||||
|
||||
###############################################################################
|
||||
|
||||
# Check out the lantern git submodule if needed
|
||||
if [ ! -d "${lantern_path}" ]; then git submodule update --init "${lantern_path}"; fi
|
||||
if [ ! -f "${lantern_path}/lantern.sh" ]; then
|
||||
echo "Checking out lantern";
|
||||
git submodule update --init "${lantern_path}";
|
||||
fi
|
||||
|
||||
source "${lantern_path}/lantern.sh";
|
||||
|
||||
|
@ -34,8 +42,11 @@ if [[ "$#" -lt 1 ]]; then
|
|||
echo -e " ./build ${CTOKEN}{action}${RS} ${CTOKEN}{action}${RS} ${CTOKEN}{action}${RS} ...";
|
||||
echo -e "";
|
||||
echo -e "${CSECTION}Available actions${RS}";
|
||||
echo -e " ${CACTION}setup${RS} - Perform initial setup";
|
||||
echo -e " ${CACTION}ci${RS} - Perform CI tasks";
|
||||
echo -e " ${CACTION}setup${RS} - Perform initial setup";
|
||||
echo -e " ${CACTION}build${RS} - Restore nuget packages & compile project";
|
||||
echo -e " ${CACTION}ci${RS} - Perform CI tasks";
|
||||
echo -e " ${CACTION}package${RS} - Pack latest build for package managers";
|
||||
echo -e " ${CACTION}archive${RS} - Construct binary archives";
|
||||
echo -e "";
|
||||
|
||||
exit 1;
|
||||
|
@ -43,36 +54,152 @@ fi
|
|||
|
||||
###############################################################################
|
||||
|
||||
function task_setup {
|
||||
task_setup() {
|
||||
task_begin "Checking environment";
|
||||
|
||||
check_command git true;
|
||||
check_command mono true;
|
||||
check_command msbuild true;
|
||||
check_command nuget true;
|
||||
check_command mktemp true;
|
||||
|
||||
task_end 0;
|
||||
}
|
||||
|
||||
function task_build {
|
||||
task_build() {
|
||||
task_begin "Restoring nuget packages";
|
||||
nuget restore;
|
||||
task_end $?;
|
||||
|
||||
task_begin "Building";
|
||||
execute msbuild;
|
||||
debug_logfile="$(mktemp --suffix ".rhinoreminds.debug.log")";
|
||||
release_logfile="$(mktemp --suffix ".rhinoreminds.release.log")";
|
||||
(
|
||||
execute msbuild /consoleloggerparameters:ForceConsoleColor >"${debug_logfile}" 2>&1
|
||||
release_exit_code=$!;
|
||||
) &
|
||||
(
|
||||
execute msbuild /consoleloggerparameters:ForceConsoleColor /p:Configuration=Release >"${release_logfile}" 2>&1
|
||||
debug_exit_code=$!;
|
||||
) &
|
||||
|
||||
wait
|
||||
|
||||
# FUTURE: Grab the
|
||||
stage_begin "Debug compilation output";
|
||||
cat "${debug_logfile}";
|
||||
stage_end "${release_exit_code}";
|
||||
|
||||
stage_begin "Release compilation output";
|
||||
cat "${release_logfile}";
|
||||
stage_end "${debug_exit_code}";
|
||||
|
||||
echo "";
|
||||
|
||||
rm "${debug_logfile}" "${release_logfile}";
|
||||
|
||||
|
||||
task_end $?;
|
||||
}
|
||||
|
||||
function task_ci {
|
||||
task_archive() {
|
||||
task_begin "Deleting extra files";
|
||||
execute find RhinoReminds/bin -iname "*.xml" -delete;
|
||||
task_end $?;
|
||||
|
||||
task_begin "Setting permissions";
|
||||
find RhinoReminds/bin -type f -print0 | xargs -0 -P4 chmod -c -x;
|
||||
find RhinoReminds/bin -type f -iname "*.exe" -print0 | xargs -0 -P4 chmod -c +x;
|
||||
task_end $?;
|
||||
|
||||
task_begin "Creating Archives";
|
||||
execute cp -ral RhinoReminds/bin/Debug RhinoReminds-Debug;
|
||||
execute cp -ral RhinoReminds/bin/Release RhinoReminds-Release;
|
||||
execute cp -al README.md RhinoReminds-Debug/README.md;
|
||||
execute cp -al README.md RhinoReminds-Release/README.md;
|
||||
|
||||
execute tar -caf RhinoReminds-Debug.tar.gz RhinoReminds-Debug;
|
||||
execute tar -caf RhinoReminds-Release.tar.gz RhinoReminds-Release;
|
||||
|
||||
execute rm -r RhinoReminds-{Debug,Release};
|
||||
task_end $?;
|
||||
|
||||
if [ "${ARCHIVE}" != "" ]; then
|
||||
task_begin "CI environment detected - moving archives to specified CI archive directory";
|
||||
|
||||
execute mv RhinoReminds-{Debug,Release}.tar.gz "${ARCHIVE}";
|
||||
|
||||
task_end $?;
|
||||
fi
|
||||
}
|
||||
|
||||
task_package() {
|
||||
task_begin "Preparing for packaging";
|
||||
check_command fpm true;
|
||||
|
||||
package_dir="$(mktemp -d --suffix "-rhinoreminds-ci-fpm-package")";
|
||||
|
||||
echo "Error: Not implemented yet :-/";
|
||||
exit 1;
|
||||
|
||||
task_end $?;
|
||||
}
|
||||
|
||||
task_deploy() {
|
||||
stage_begin "Deploying to ${deploy_ssh_host}....";
|
||||
if [ "${SSH_KEY_PATH}" == "" ]; then
|
||||
stage_end 1 "Error: Can't find the SSH key as the environment variable SSH_KEY_PATH isn't set.";
|
||||
fi
|
||||
|
||||
task_begin "Preparing upload";
|
||||
|
||||
subtask_begin "Creating temporary directory";
|
||||
temp_dir="$(mktemp -d --suffix "-rhinoreminds-upload")";
|
||||
subtask_end $? "Error: Failed to create temporary directory";
|
||||
|
||||
subtask_begin "Unpacking release files";
|
||||
execute tar -xf "${ARCHIVE}/RhinoReminds-Release.tar.gz" -C "${temp_dir}";
|
||||
subtask_end $? "Failed to unpack release files";
|
||||
|
||||
# Define the directory whose contents we want to upload
|
||||
source_upload_dir="${temp_dir}/RhinoReminds-Release";
|
||||
|
||||
task_end $?;
|
||||
|
||||
task_begin "Uploading release";
|
||||
# TODO: If we experience issues, we need to somehow figure out how to recursively delete the contents of the directory before uploading. We may have to install lftp and use that instead
|
||||
sftp -i "${SSH_KEY_PATH}" -P "${deploy_ssh_port}" -o PasswordAuthentication=no "${deploy_ssh_user}@${deploy_ssh_host}" << SFTPCOMMANDS
|
||||
rm ${deploy_target_dir}/*
|
||||
put -r ${source_upload_dir}/* ${deploy_target_dir}
|
||||
bye
|
||||
SFTPCOMMANDS
|
||||
task_end $? "Failed to upload release";
|
||||
|
||||
task_begin "Cleaning up";
|
||||
execute rm -r "${temp_dir}";
|
||||
task_end $?;
|
||||
|
||||
stage_end $? "Failed to deploy to ${deploy_ssh_host}.";
|
||||
}
|
||||
|
||||
task_ci() {
|
||||
tasks_run setup;
|
||||
|
||||
task_begin "Environment Information";
|
||||
execute git --version;
|
||||
execute uname -a;
|
||||
execute git --version;
|
||||
execute mono --version;
|
||||
execute nuget help | head -n1;
|
||||
task_end 0;
|
||||
|
||||
tasks_run build;
|
||||
# FUTURE: We'll (eventually) want to call the package task here too
|
||||
tasks_run build archive;
|
||||
|
||||
if [ "${GIT_REF_NAME}" == "refs/heads/master" ]; then
|
||||
tasks_run deploy;
|
||||
else
|
||||
echo "Not deploying, as this build wasn't on the master branch.";
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 37e1d0ea747ffce5f4ed3270c150db933164a777
|
||||
Subproject commit 411df752dd800490dd4a627f5ef6df9bace5341a
|
8
logrotate
Normal file
8
logrotate
Normal file
|
@ -0,0 +1,8 @@
|
|||
/var/log/rhinoreminds/rhinoreminds.log {
|
||||
rotate 12
|
||||
weekly
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
}
|
2
rhinoreminds-rsyslog.conf
Normal file
2
rhinoreminds-rsyslog.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
if $programname == 'rhinoreminds' then /var/log/rhinoreminds/rhinoreminds.log
|
||||
if $programname == 'rhinoreminds' then ~
|
|
@ -2,15 +2,64 @@
|
|||
Description=RhinoReminds XMPP Bot
|
||||
After=network.target prosody.service
|
||||
|
||||
# No more than 5 crashes in 12 hours
|
||||
StartLimitIntervalSec=43200
|
||||
StartLimitBurst=5
|
||||
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Another Type option: forking
|
||||
User=rhinoreminds
|
||||
WorkingDirectory=/srv/rhinoreminds
|
||||
ExecStart=/srv/rhinoreminds/start_service.sh
|
||||
Restart=on-failure
|
||||
Type=forking
|
||||
PIDFile=/run/rhinoreminds/rhinoreminds.pid
|
||||
# We change our own user
|
||||
User=root
|
||||
WorkingDirectory=/srv/kraggwapple
|
||||
ExecStart=/srv/kraggwapple/start_service.sh
|
||||
Restart=always
|
||||
# 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`
|
||||
#StandardOutput=syslog
|
||||
#StandardError=syslog
|
||||
#SyslogIdentifier=rhinoreminds
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
#!/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_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 --auto-exit --domain starbeamrainbowlabs.com --avatar avatar.png & echo "$!" >/run/rhinoreminds/rhinoreminds.pid; disown'
|
||||
|
|
Loading…
Reference in a new issue