Compare commits

...

62 Commits
v0.1 ... master

Author SHA1 Message Date
Starbeamrainbowlabs c13dd77b5f
Empty commit to test build system
continuous-integration/laminar-elessar Build 40 succeeded in 1 minute 8 seconds . Details
2019-05-08 15:59:55 +01:00
Starbeamrainbowlabs 3b072f3e97
Bugfix: Don't say we're late if we aren't 2019-05-08 15:44:46 +01:00
Starbeamrainbowlabs 3d4af3eb2f
Add 1s grace time for processing to sorry I'm late message
continuous-integration/laminar-elessar Build 39 succeeded in 1 minute 14 seconds . Details
2019-03-31 12:14:15 +01:00
Starbeamrainbowlabs ef8c90b4ad
Remove debug call in CI script
continuous-integration/laminar-elessar Build 38 succeeded in 1 minute 37 seconds . Details
2019-03-29 00:08:09 +00:00
Starbeamrainbowlabs b47c7e4cf7
Properly detect branch name using the git-repo environment variable
continuous-integration/laminar-elessar Build 37 succeeded in 1 minute 31 seconds . Details
2019-03-29 00:05:05 +00:00
Starbeamrainbowlabs 23be94ff5e
Add debug logging to ci script
continuous-integration/laminar-elessar Build 36 succeeded in 47 seconds . Details
2019-03-28 23:07:48 +00:00
Starbeamrainbowlabs eeea59eaff
Add --auto-exit CLI param
continuous-integration/laminar-elessar Build 35 succeeded in 47 seconds . Details
2019-03-28 23:04:52 +00:00
Starbeamrainbowlabs dced63f02c
Don't deploy if we're on master
continuous-integration/laminar-elessar Build 34 succeeded in 52 seconds . Details
2019-03-28 23:00:05 +00:00
Starbeamrainbowlabs c9d2f1b939
Turns out I didn't understand how SortedSet IComparers work :P
continuous-integration/laminar-elessar Build 33 succeeded in 1 minute 3 seconds . Details
2019-03-28 20:35:28 +00:00
Starbeamrainbowlabs 141bccc03c
Embed git commit hash in binary & output version number
continuous-integration/laminar-elessar Build 32 succeeded in 1 minute 1 second . Details
2019-03-28 20:03:10 +00:00
Starbeamrainbowlabs ab19b82d35
Update NuGet packages
continuous-integration/laminar-elessar Build 31 succeeded in 1 minute 2 seconds . Details
2019-03-28 19:34:49 +00:00
Starbeamrainbowlabs 48c62f53e1
Update lantern build engine
continuous-integration/laminar-elessar Build 30 succeeded in 1 minute 2 seconds . Details
2019-03-28 19:30:05 +00:00
Starbeamrainbowlabs 23fb6cf820
Bugfix: Actually get the really right reminder this time....hopefully
continuous-integration/laminar-elessar Build 29 succeeded in 1 minute 4 seconds . Details
Also add continuous deployment! :D :D :D
2019-03-28 19:27:54 +00:00
Starbeamrainbowlabs b7f3f27042
Add logrotate script
continuous-integration/laminar-elessar Build 28 succeeded in 53 seconds . Details
2019-03-14 22:31:02 +00:00
Starbeamrainbowlabs 64bb9550b3
Bugfix: Wait in the right reminder next, fix regexes
continuous-integration/laminar-elessar Build 77 succeeded in 46 seconds . Details
2019-03-11 21:21:18 +00:00
Starbeamrainbowlabs 23a9fa3ed7
Change it back, because it's not working right.
continuous-integration/laminar-elessar Build 76 succeeded in 44 seconds . Details
2019-03-11 20:51:29 +00:00
Starbeamrainbowlabs 49316836d9
Merge branch 'master' of git.starbeamrainbowlabs.com:sbrl/RhinoReminds
continuous-integration/laminar-elessar Build 75 succeeded in 55 seconds . Details
2019-03-11 20:41:54 +00:00
Starbeamrainbowlabs a256158f89
Tweak Task.Delay() to fix odd issue 2019-03-11 20:41:49 +00:00
Starbeamrainbowlabs e60dd12751
Bugfix: Don't crash if an avatar isn't requested
continuous-integration/laminar-elessar Build 66 succeeded in 51 seconds . Details
2019-03-04 14:40:12 +00:00
Starbeamrainbowlabs 8099155455
Change compilation target to AnyCPU
continuous-integration/laminar-elessar Build 65 succeeded in 51 seconds . Details
2019-03-03 12:37:35 +00:00
Starbeamrainbowlabs b32999f80e
Update Microsoft.Recognisers.Text
continuous-integration/laminar-elessar Build 64 succeeded in 1 minute 13 seconds . Details
2019-03-03 12:23:48 +00:00
Starbeamrainbowlabs c3908ec2c7
Start writing package build task, but it's not finished yet
continuous-integration/laminar-elessar Build 63 succeeded in 48 seconds . Details
2019-02-17 23:35:06 +00:00
Starbeamrainbowlabs 39420d70d5
Empty commit to test build system
continuous-integration/laminar-elessar Build 62 succeeded in 41 seconds . Details
2019-02-17 16:24:27 +00:00
Starbeamrainbowlabs 44c809eb75
Empty commit to test build system 2019-02-17 16:08:02 +00:00
Starbeamrainbowlabs 9b9dae00f8
Empty commit to test build system 2019-02-17 16:08:00 +00:00
Starbeamrainbowlabs 29c307e297
Empty commit to test build system 2019-02-17 16:07:59 +00:00
Starbeamrainbowlabs 6ec324ef7c
Empty commit to test build system 2019-02-17 16:04:42 +00:00
Starbeamrainbowlabs 9343ce875c
Add link to associated blog post to README.md 2019-02-17 15:58:18 +00:00
Starbeamrainbowlabs e23b2e0f3c
Update lantern build engine again 2019-02-12 20:34:37 +00:00
Starbeamrainbowlabs efffaa7465
Update lantern build engine 2019-02-12 20:30:58 +00:00
Starbeamrainbowlabs de1cc6bb51
Run debug & release builds in parallel 2019-02-12 20:22:28 +00:00
Starbeamrainbowlabs 83c1246fab
Actually build for release mya 2019-02-12 20:08:27 +00:00
Starbeamrainbowlabs fd74e5f3a7
Oops 2019-02-12 20:01:15 +00:00
Starbeamrainbowlabs 7a5a43f9a2
Add more debugging statements to try & figure out what's going wrong 2019-02-12 19:59:57 +00:00
Starbeamrainbowlabs 22c2212952
[CI] Where _are_ we? 2019-02-12 19:51:59 +00:00
Starbeamrainbowlabs 950d12f08b
Hrm. The release binary archive does't appear to be generating correctly.
Will this fix it?
2019-02-12 19:50:16 +00:00
Starbeamrainbowlabs 48cc42ef2a
CI: Create archives after build automatically 2019-02-12 19:44:37 +00:00
Starbeamrainbowlabs 1360eb9c6f
[build] Add ability to create archives automagically 2019-02-12 19:43:40 +00:00
Starbeamrainbowlabs 1f88789309
Update lantern build engine 2019-02-12 19:30:13 +00:00
Starbeamrainbowlabs 09669d9acf
Document assorted files that are useful for running RhinoReminds as a system service 2019-02-12 19:28:31 +00:00
Starbeamrainbowlabs a4d996fb3d
Bugfix: Fix crash if the next reminder is deleted before it can be sent.
Also enable some debug logging, just in case it acts up again.
2019-02-08 11:30:16 +00:00
Starbeamrainbowlabs b5deb649dc
Display message on startup to aid clarity in logs 2019-02-08 11:02:52 +00:00
Starbeamrainbowlabs 6d400b7c3a
[ci] Let's try making the auto-checkout test stricter 2019-02-08 00:40:04 +00:00
Starbeamrainbowlabs 46f55a086b
Is the auto-checkout working? 2019-02-08 00:38:50 +00:00
Starbeamrainbowlabs 20f5329b9c
Empty commit to test build system 2019-02-08 00:36:42 +00:00
Starbeamrainbowlabs 7ae561a050
Empty commit to test build system 2019-02-08 00:31:31 +00:00
Starbeamrainbowlabs 99101cd519
[ci] Display version of mono 2019-02-08 00:27:49 +00:00
Starbeamrainbowlabs fef5dcbc79
Merge branch 'master' of git.starbeamrainbowlabs.com:sbrl/RhinoReminds 2019-02-08 00:25:24 +00:00
Starbeamrainbowlabs c5e54864bc
Formatting in service file 2019-02-08 00:24:45 +00:00
Starbeamrainbowlabs 6f9ff7eef0
Merge branch 'master' of git.starbeamrainbowlabs.com:sbrl/RhinoReminds 2019-02-04 13:54:34 +00:00
Starbeamrainbowlabs 517e3dcafc
Update systemd config to optionally use rsyslog 2019-02-04 13:54:21 +00:00
Starbeamrainbowlabs 97a6d95902
start_service.sh: Apparently sudo doesn't quit - and stays as a wrapper process.Solve this by disowning after sudo & avoid showing the password in the sudo process arguments by preserving the environment when sudoing. 2019-02-01 22:26:22 +00:00
Starbeamrainbowlabs 1314b3ab75
Update service files etc. 2018-12-27 14:05:40 +00:00
Starbeamrainbowlabs 7cbf1ef79c
Generate error on unknown argument 2018-12-27 13:38:23 +00:00
Starbeamrainbowlabs 430b5082dd
Add --pidfile support, and output error properly if XMPP_JID or XMPP_PASSWORD aren't set 2018-12-27 13:36:55 +00:00
Starbeamrainbowlabs 579078edf0
Change the internal backing list to a SortedSet to reduce complexity & increase stability 2018-12-27 12:29:41 +00:00
Starbeamrainbowlabs adfb482072
Bugfix: Notify the reminder thread when a reminder gets deleted from the list 2018-12-22 16:05:45 +00:00
Starbeamrainbowlabs 4174ec85c6
Fix the reconnection logic. Hooray! 2018-12-21 10:58:14 +00:00
Starbeamrainbowlabs 505e500634
Bugfix: COrrect find-and-replace ordering.
More work may be needed here.... :-"
'
2018-12-19 17:46:53 +00:00
Starbeamrainbowlabs b5df7ab94e
README: Layout 2018-12-06 00:21:43 +00:00
Starbeamrainbowlabs dd96754126
Fill out more of the README 2018-12-06 00:20:58 +00:00
Starbeamrainbowlabs e58f56b06d
README: Add link to releases page 2018-12-06 00:14:36 +00:00
18 changed files with 968 additions and 197 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
git-hash.txt
*.tar.gz
*.backup
# Created by https://www.gitignore.io/api/monodevelop,visualstudio,csharp,git # Created by https://www.gitignore.io/api/monodevelop,visualstudio,csharp,git
# Edit at https://www.gitignore.io/?templates=monodevelop,visualstudio,csharp,git # Edit at https://www.gitignore.io/?templates=monodevelop,visualstudio,csharp,git

View File

@ -2,7 +2,15 @@
> An XMPP reminder bot written in C#. > 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 ## 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 +40,14 @@ mono [--debug] RhinoReminds.exe [--help]
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 ## 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.
@ -61,6 +77,17 @@ Delete number eight
Delete reminders 2, 3, 4, and 7 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)
- [S22.Xmpp API Documentation](https://smiley22.github.io/S22.Xmpp/Documentation/) - [S22.Xmpp API Documentation](https://smiley22.github.io/S22.Xmpp/Documentation/)

View File

@ -9,7 +9,6 @@ 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;
@ -24,35 +23,35 @@ namespace RhinoReminds
public event OnConnectedHandler OnConnected; public event OnConnectedHandler OnConnected;
public readonly string Jid; public readonly Jid Jid;
public string Username => Jid.Split('@')[0]; public string Username => Jid.Node;
public string Hostname => Jid.Split('@')[1]; public string Hostname => Jid.Domain;
private readonly string password; private readonly string password;
public readonly List<string> AllowedDomains = new List<string>(); public readonly List<string> AllowedDomains = new List<string>();
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;
} }
@ -64,26 +63,48 @@ namespace RhinoReminds
reminderList = ReminderList.FromXmlFile(ReminderFilePath); reminderList = ReminderList.FromXmlFile(ReminderFilePath);
} }
client = new XmppClient(Hostname, Username, password);
client.Error += errorHandler;
client.Message += messageHandlerRoot;
client.SubscriptionRequest += subscriptionRequestHandler;
// Connect to the server. This starts it's own thread that doesn't block the program exiting, apparently // Connect to the server. This starts it's own thread that doesn't block the program exiting, apparently
await connect(); await connect();
//client.SetStatus(Availability.Online); client.SetStatus(Availability.Online);
await watchForReminders(); await watchForReminders();
} }
private async Task reconnect()
{
// 1: Ensure we're disconnected from the server.
disconnect();
float nextBackoffDelay = defaultBackoffDelay;
do {
nextBackoffDelay *= backoffDelayMultiplier;
Console.Error.WriteLine($"[Rhino/Reconnect] Reconnecting in {TimeSpan.FromSeconds(nextBackoffDelay).ToString()}.");
Thread.Sleep((int)(nextBackoffDelay * 1000));
Console.WriteLine("[Rhino/Reconnect] Attempting to reconnect to the server");
} while (!await connect());
}
private async Task<bool> connect() private async Task<bool> connect()
{ {
if (client.Connected) if (client != null) {
return true; if (client.Connected) {
return true;
} else {
client.Dispose();
client = null;
}
}
DateTime startTime = DateTime.Now; DateTime startTime = DateTime.Now;
client.Connect(); client = new SimpleXmppClient(Jid, password);
client.Error += errorHandler;
client.Message += messageHandlerRoot;
client.SubscriptionRequest += subscriptionRequestHandler;
client.Connect("RhinoReminds");
while (!client.Connected) while (!client.Connected)
{ {
@ -94,17 +115,25 @@ namespace RhinoReminds
} }
Console.WriteLine($"[Rhino/Setup] Connected as {Jid}"); Console.WriteLine($"[Rhino/Setup] Connected as {Jid}");
OnConnected(this, new OnConnectedEventArgs()); OnConnected?.Invoke(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}");
@ -115,17 +144,8 @@ namespace RhinoReminds
{ {
Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}"); Console.Error.WriteLine($"[Error] {e.Reason}: {e.Exception}");
if(!client.Connected || e.Exception is IOException) if(!client.Connected || e.Exception is IOException) {
{ reconnect().Wait();
Console.Error.WriteLine($"[Error/Handler] Reconnecting in {TimeSpan.FromSeconds(nextBackoffDelay).ToString()}.");
Thread.Sleep(nextBackoffDelay * 1000);
Console.WriteLine("[Error/Handler] Attempting to reconnect to the server");
if (!connect().Result)
nextBackoffDelay = (int)Math.Ceiling(nextBackoffDelay * backoffDelayMultiplier);
else
nextBackoffDelay = defaultBackoffDelay;
} }
} }
@ -140,8 +160,8 @@ namespace RhinoReminds
catch (Exception error) catch (Exception error)
{ {
Console.Error.WriteLine(error); Console.Error.WriteLine(error);
sendChatReply(eventArgs.Message, "Oops! I encountered an error. Please report this to my operator!"); client.SendChatReply(eventArgs.Message, "Oops! I encountered an error. Please report this to my operator!");
sendChatReply(eventArgs.Message, $"Technical details: {WebUtility.HtmlEncode(error.ToString())} (stack trace available in server log) "); client.SendChatReply(eventArgs.Message, $"Technical details: {WebUtility.HtmlEncode(error.ToString())} (stack trace available in server log) ");
} }
} }
@ -160,7 +180,7 @@ namespace RhinoReminds
private void messageHandler(Message message) private void messageHandler(Message message)
{ {
if (!AllowedDomains.Contains("*") && !AllowedDomains.Contains(message.From.Domain)) { if (!AllowedDomains.Contains("*") && !AllowedDomains.Contains(message.From.Domain)) {
sendChatMessage(message.From, "Sorry! The domain of your JID doesn't match the ones in my allowed list."); client.SendChatMessage(message.From, "Sorry! The domain of your JID doesn't match the ones in my allowed list.");
return; return;
} }
@ -172,15 +192,20 @@ 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, "**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; break;
case "delete": case "delete":
@ -201,19 +226,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");
@ -222,7 +249,7 @@ namespace RhinoReminds
} }
listMessage.AppendLine(); listMessage.AppendLine();
listMessage.AppendLine($"({userReminderList.Count()} total)"); listMessage.AppendLine($"({userReminderList.Count()} total)");
sendChatReply(message, listMessage.ToString()); client.SendChatReply(message, listMessage.ToString());
break; break;
@ -231,8 +258,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(
@ -245,136 +272,144 @@ namespace RhinoReminds
@"^remind\s+(?:me\s+)?", @"^remind\s+(?:me\s+)?",
@"^me\s+", @"^me\s+",
@"^on\s+", @"^on\s+",
@"my", @"\byou\b",
@"you", @"\byour\b",
@"your", @"\bmy\b",
@"&" // 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()
{
CancellationTokenSource oldToken = reminderWatcherReset;
reminderWatcherReset = new CancellationTokenSource();
oldToken?.Cancel();
}
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();
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(); Reminder newNextReminder = reminderList.GetNextReminder();
//Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing."); Console.WriteLine("[Rhino/Reminderd/Canceller] Reminder added - comparing.");
//Console.WriteLine($"[Rhino/Reminderd/Canceller] {nextReminder} / {newNextReminder}"); Console.WriteLine($"[Rhino/Reminderd/Canceller] {nextReminder} / {newNextReminder}");
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();
TimeSpan nextWaitingTime; // Wait for the next reminder
TimeSpan nextWaitingTime = new TimeSpan();
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}"); {
await Task.Delay(nextWaitingTime, cancellationToken); 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) { else {
//Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating"); Console.WriteLine("[Rhino/Reminderd] Sleeping until interrupted");
cancellationSource = new CancellationTokenSource(); await Task.Delay(Timeout.Infinite, reminderWatcherResetToken);
cancellationToken = cancellationSource.Token; }
}
catch (TaskCanceledException)
{
Console.WriteLine("[Rhino/Reminderd] Sleep interrupted, recalculating");
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(); continue;
cancellationToken = cancellationSource.Token; }
if (nextReminder.Time > DateTime.Now.AddSeconds(-1)) {
Console.WriteLine("[Rhino/Reminderd] Didn't sleep for long enough, going back to bed *yawn*");
continue; continue;
} }
Console.WriteLine($"[Rhino/Reminderd] Sending notification {nextReminder}"); Console.WriteLine($"[Rhino/Reminderd] Sending notification {nextReminder}");
Jid targetJid = nextReminder.Jid; // Apparently nextReminder is null after the sendAndDeleteReminder() call - very odd!
sendAndDeleteReminder(nextReminder);
try if (nextWaitingTime.TotalMilliseconds < -1 * 2000) {
{ client.SendChatMessage(
sendChatMessage( targetJid,
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,
"(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
} }
} }

View 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);
}
}
}

View File

@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using RhinoReminds.Utilities;
using S22.Xmpp; using S22.Xmpp;
using S22.Xmpp.Client; using S22.Xmpp.Client;
using S22.Xmpp.Im; using S22.Xmpp.Im;
using SBRL.Utilities;
namespace RhinoReminds namespace RhinoReminds
{ {
@ -15,12 +19,21 @@ 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 bool ExitOnModify = false;
public string Password = null; public string Password = null;
} }
public static class Program 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(); private static ProgramSettings settings = new ProgramSettings();
public static int Main(string[] args) public static int Main(string[] args)
@ -36,7 +49,7 @@ namespace RhinoReminds
switch (args[i]) { switch (args[i]) {
case "-h": case "-h":
case "--help": case "--help":
Console.WriteLine("--- RhinoReminds ---"); Console.WriteLine($"--- RhinoReminds {Version} ---");
Console.WriteLine("> An XMPP reminder bot"); Console.WriteLine("> An XMPP reminder bot");
Console.WriteLine(" By Starbeamrainbowlabs"); Console.WriteLine(" By Starbeamrainbowlabs");
Console.WriteLine(); Console.WriteLine();
@ -44,10 +57,12 @@ namespace RhinoReminds
Console.WriteLine(" mono RhinoReminds.exe {options}"); Console.WriteLine(" mono RhinoReminds.exe {options}");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Options:"); Console.WriteLine("Options:");
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(" --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();
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,33 +82,96 @@ namespace RhinoReminds
case "--avatar": case "--avatar":
settings.AvatarFilepath = args[++i]; settings.AvatarFilepath = args[++i];
break; 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.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()
{
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) { ClientListener client = new ClientListener(settings.Jid, settings.Password) {
ReminderFilePath = settings.Filepath ReminderFilePath = settings.Filepath
}; };
client.AllowedDomains.Add(settings.AllowedDomain); client.AllowedDomains.Add(settings.AllowedDomain);
// Update the avatar if appropriate // Update the avatar if appropriate
if (settings.AvatarFilepath != string.Empty) { if (settings.AvatarFilepath != string.Empty) {
client.OnConnected += (object sender, OnConnectedEventArgs eventArgs) => { OnConnectedHandler handler = null;
handler = (object sender, OnConnectedEventArgs eventArgs) => {
client.SetAvatar(settings.AvatarFilepath); client.SetAvatar(settings.AvatarFilepath);
Console.WriteLine($"[Program] Set avatar to '{settings.AvatarFilepath}'."); Console.WriteLine($"[Program] Set avatar to '{settings.AvatarFilepath}'.");
client.OnConnected -= handler;
}; };
client.OnConnected += handler;
} }
// Connect to the server & start listening // Connect to the server & start listening
// Make sure the program doesn't exit whilst we're connected // Make sure the program doesn't exit whilst we're connected
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;
}
} }
} }
} }

View File

@ -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
} }
} }

View File

@ -1,20 +1,20 @@
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;
namespace RhinoReminds 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 public class ReminderList
{ {
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 +29,30 @@ 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);
OnReminderListUpdate(this, result); Reminders.Add(result);
OnReminderListUpdate(this, new ReminderListUpdateEventArgs());
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!");
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 // 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 +75,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,20 +90,28 @@ 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;
} }
#endregion #endregion
public override string ToString()
{
string result = "Reminder list:";
foreach (Reminder nextReminder in Reminders)
Console.WriteLine($" - {nextReminder}");
return result;
}
} }
} }

View File

@ -19,7 +19,6 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<ExternalConsole>true</ExternalConsole> <ExternalConsole>true</ExternalConsole>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<Optimize>true</Optimize> <Optimize>true</Optimize>
@ -27,7 +26,6 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<ExternalConsole>true</ExternalConsole> <ExternalConsole>true</ExternalConsole>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
@ -43,19 +41,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.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>
<Reference Include="Microsoft.Recognizers.Text"> <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>
<Reference Include="Microsoft.Recognizers.Text.Number"> <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>
<Reference Include="Microsoft.Recognizers.Text.NumberWithUnit"> <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>
<Reference Include="Microsoft.Recognizers.Text.DateTime"> <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> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -68,6 +66,10 @@
<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" />
<Compile Include="Utilities\EmbeddedFiles.cs" />
<Compile Include="Utilities\ExitWatcher.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
@ -76,4 +78,11 @@
<Folder Include="Utilities\" /> <Folder Include="Utilities\" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Target Name="BeforeBuild" BeforeTargets="Build">
<Exec Command="git rev-parse HEAD &gt;git-hash.txt" WorkingDirectory="$(ProjectDir)" IgnoreExitCode="true" />
</Target>
<ItemGroup>
<EmbeddedResource Include="git-hash.txt" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,42 @@
using System;
using S22.Xmpp;
using S22.Xmpp.Client;
using S22.Xmpp.Im;
namespace RhinoReminds
{
public class SimpleXmppClient : XmppClient
{
public SimpleXmppClient(Jid user, string password) : base(user.Domain, user.Node, password)
{
}
/// <summary>
/// Sends a chat message to the specified JID.
/// </summary>
/// <param name="to">The JID to send the message to.</param>
/// <param name="message">The messaage to send.</param>
public void SendChatMessage(Jid to, string message)
{
//Console.WriteLine($"[Rhino/Send/Chat] Sending {message} -> {to}");
SendMessage(
to, message,
null, null, MessageType.Chat
);
}
/// <summary>
/// Sends a chat message in direct reply to a given incoming message.
/// </summary>
/// <param name="originalMessage">Original message.</param>
/// <param name="reply">Reply.</param>
public void SendChatReply(Message originalMessage, string reply)
{
//Console.WriteLine($"[Rhino/Send/Reply] Sending {reply} -> {originalMessage.From}");
SendMessage(
originalMessage.From, reply,
null, originalMessage.Thread, MessageType.Chat
);
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View File

@ -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.4" targetFramework="net47" /> <package id="Microsoft.Recognizers.Text" version="1.1.6" targetFramework="net47" />
<package id="Microsoft.Recognizers.Text.DateTime" version="1.1.4" targetFramework="net47" /> <package id="Microsoft.Recognizers.Text.DateTime" version="1.1.6" targetFramework="net47" />
<package id="Microsoft.Recognizers.Text.Number" version="1.1.4" targetFramework="net47" /> <package id="Microsoft.Recognizers.Text.Number" version="1.1.6" targetFramework="net47" />
<package id="Microsoft.Recognizers.Text.NumberWithUnit" version="1.1.4" 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="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" />

145
build
View File

@ -18,10 +18,18 @@ lantern_path="./lantern-build-engine";
# Put any custom settings here. # Put any custom settings here.
build_output_folder="./dist"; 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 # 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"; 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 " ./build ${CTOKEN}{action}${RS} ${CTOKEN}{action}${RS} ${CTOKEN}{action}${RS} ...";
echo -e ""; echo -e "";
echo -e "${CSECTION}Available actions${RS}"; echo -e "${CSECTION}Available actions${RS}";
echo -e " ${CACTION}setup${RS} - Perform initial setup"; echo -e " ${CACTION}setup${RS} - Perform initial setup";
echo -e " ${CACTION}ci${RS} - Perform CI tasks"; 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 ""; echo -e "";
exit 1; exit 1;
@ -43,36 +54,152 @@ fi
############################################################################### ###############################################################################
function task_setup { task_setup() {
task_begin "Checking environment"; task_begin "Checking environment";
check_command git true; check_command git true;
check_command mono true;
check_command msbuild true; check_command msbuild true;
check_command nuget true; check_command nuget true;
check_command mktemp true;
task_end 0; task_end 0;
} }
function task_build { task_build() {
task_begin "Restoring nuget packages"; task_begin "Restoring nuget packages";
nuget restore; nuget restore;
task_end $?; task_end $?;
task_begin "Building"; 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 $?; 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; tasks_run setup;
task_begin "Environment Information"; task_begin "Environment Information";
execute git --version;
execute uname -a; execute uname -a;
execute git --version;
execute mono --version;
execute nuget help | head -n1; execute nuget help | head -n1;
task_end 0; 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
View File

@ -0,0 +1,8 @@
/var/log/rhinoreminds/rhinoreminds.log {
rotate 12
weekly
missingok
notifempty
compress
delaycompress
}

View File

@ -0,0 +1,2 @@
if $programname == 'rhinoreminds' then /var/log/rhinoreminds/rhinoreminds.log
if $programname == 'rhinoreminds' then ~

View File

@ -2,15 +2,64 @@
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
Restart=on-failure ExecStart=/srv/kraggwapple/start_service.sh
Restart=always
# Other Restart options: or always, on-abort, etc # Other Restart options: or always, on-abort, etc
# Delay restarts by 60 seconds
RestartSec=60
[Install] [Install]
WantedBy=multi-user.target 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

View File

@ -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 --auto-exit --domain starbeamrainbowlabs.com --avatar avatar.png & echo "$!" >/run/rhinoreminds/rhinoreminds.pid; disown'