diff --git a/ChatSharp/IrcClient.cs b/ChatSharp/IrcClient.cs index 1eb1b5e..2649ff4 100644 --- a/ChatSharp/IrcClient.cs +++ b/ChatSharp/IrcClient.cs @@ -163,8 +163,12 @@ namespace ChatSharp Users = new UserPool(); Users.Add(User); // Add self to user pool Capabilities = new CapabilityPool(); + // List of supported capabilities - Capabilities.AddRange(new string[] { "server-time", "multi-prefix", "cap-notify" }); + Capabilities.AddRange(new string[] { + "server-time", "multi-prefix", "cap-notify", "znc.in/server-time", "znc.in/server-time-iso" + }); + IsNegotiatingCapabilities = false; } diff --git a/ChatSharp/IrcMessage.cs b/ChatSharp/IrcMessage.cs index f273af1..609d626 100644 --- a/ChatSharp/IrcMessage.cs +++ b/ChatSharp/IrcMessage.cs @@ -29,6 +29,10 @@ namespace ChatSharp /// The message tags. /// public KeyValuePair[] Tags { get; private set; } + /// + /// The message timestamp in ISO 8601 format. + /// + public Timestamp Timestamp { get; private set; } /// /// Initializes and decodes an IRC message, given the raw message from the server. @@ -98,6 +102,22 @@ namespace ChatSharp Command = rawMessage; Parameters = new string[0]; } + + // Parse server-time message tag. + // Fallback to server-info if both znc.in/server-info and the former exists. + // + // znc.in/server-time tag + if (Tags.Any(tag => tag.Key == "t")) + { + var tag = Tags.SingleOrDefault(x => x.Key == "t"); + Timestamp = new Timestamp(tag.Value, true); + } + // server-time tag + else if (Tags.Any(tag => tag.Key == "time")) + { + var tag = Tags.SingleOrDefault(x => x.Key == "time"); + Timestamp = new Timestamp(tag.Value); + } } } } diff --git a/ChatSharp/Timestamp.cs b/ChatSharp/Timestamp.cs new file mode 100644 index 0000000..1d6bf5a --- /dev/null +++ b/ChatSharp/Timestamp.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; + +namespace ChatSharp +{ + /// + /// Represents a message timestamp received from a server. + /// + public class Timestamp + { + /// + /// A date representation of the timestamp. + /// + public DateTime Date { get; internal set; } + /// + /// A unix epoch representation of the timestamp. + /// + public double UnixTimestamp { get; internal set; } + + /// + /// Initializes and parses the timestamp received from the server. + /// + /// + /// Enable pre-ZNC 1.0 compatibility. In previous versions of the tag, + /// servers sent a unix timestamp instead of a ISO 8601 string. + internal Timestamp(string date, bool compatibility = false) + { + if (!compatibility) + { + DateTime parsedDate; + if (!DateTime.TryParseExact(date, @"yyyy-MM-dd\THH:mm:ss.fff\Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedDate)) + throw new ArgumentException("The date string was provided in an invalid format.", date); + + Date = parsedDate; + UnixTimestamp = Date.Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds; + } + else + { + double parsedTimestamp; + if (!double.TryParse(date, out parsedTimestamp)) + throw new ArgumentException("The timestamp string was provided in an invalid format.", date); + + UnixTimestamp = parsedTimestamp; + Date = (new DateTime(1970, 1, 1, 0, 0, 0).AddSeconds(UnixTimestamp)); + } + } + + /// + /// True if this timestamp is equal to another (compares unix timestamps). + /// + public bool Equals(Timestamp other) + { + return other.UnixTimestamp == UnixTimestamp; + } + + /// + /// True if this timestamp is equal to another (compares unix timestamps). + /// + public override bool Equals(object obj) + { + if (obj is Timestamp) + return Equals((Timestamp)obj); + return false; + } + + /// + /// Returns a ISO 8601 string representation of the timestamp. + /// + public string ToISOString() + { + return Date.ToString(@"yyyy-MM-dd\THH:mm:ss.fff\Z"); + } + + /// + /// Returns the hash code of the unix timestamp. + /// + public override int GetHashCode() + { + return UnixTimestamp.GetHashCode(); + } + } +} diff --git a/ChatSharpTests/IrcMessageTests.cs b/ChatSharpTests/IrcMessageTests.cs index 8539847..6ea3f18 100644 --- a/ChatSharpTests/IrcMessageTests.cs +++ b/ChatSharpTests/IrcMessageTests.cs @@ -81,5 +81,45 @@ namespace ChatSharp.Tests }; CollectionAssert.AreEqual(fromMessage.Tags, compareTags); } + + [TestMethod] + public void Timestamp_CompareISOString() + { + IrcMessage[] messages = { + new IrcMessage("@time=2011-10-19T16:40:51.620Z :Angel!angel@example.org PRIVMSG Wiz :Hello"), + new IrcMessage("@time=2012-06-30T23:59:59.419Z :John!~john@1.2.3.4 JOIN #chan") + }; + + string[] timestamps = { + "2011-10-19T16:40:51.620Z", + "2012-06-30T23:59:59.419Z" + }; + + Assert.AreEqual(messages[0].Timestamp.ToISOString(), timestamps[0]); + Assert.AreEqual(messages[1].Timestamp.ToISOString(), timestamps[1]); + } + + [TestMethod] + public void Timestamp_FromTimestamp() + { + IrcMessage[] messages = { + new IrcMessage("@t=1504923966 :Angel!angel@example.org PRIVMSG Wiz :Hello"), + new IrcMessage("@t=1504923972 :John!~john@1.2.3.4 JOIN #chan") + }; + + string[] timestamps = { + "2017-09-09T02:26:06.000Z", + "2017-09-09T02:26:12.000Z" + }; + + Assert.AreEqual(messages[0].Timestamp.ToISOString(), timestamps[0]); + Assert.AreEqual(messages[1].Timestamp.ToISOString(), timestamps[1]); + } + + [TestMethod] + public void Timestamp_FailOnLeap() + { + Assert.ThrowsException(() => new IrcMessage("@time=2012-06-30T23:59:60.419Z :John!~john@1.2.3.4 JOIN #chan")); + } } }