예제 #1
0
        protected virtual bool CheckAndHandleCooldown(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            if (Config.CooldownUpperBoundary < 0)
            {
                // the cooldown feature is not being used
                return(false);
            }

            CooldownState cdState;

            if (!ChannelToCooldown.TryGetValue(args.Channel, out cdState))
            {
                cdState = new CooldownState();
                ChannelToCooldown[args.Channel] = cdState;
            }

            cdState.CooldownValue += Config.CooldownPerCommandUsage;

            bool coolingDown = (cdState.CooldownTriggered)
                ? (cdState.CooldownValue > 0)
                : (cdState.CooldownValue > Config.CooldownUpperBoundary);

            if (coolingDown)
            {
                cdState.CooldownTriggered = true;
                string cdAnswer = Config.CooldownAnswers[ChosenRNG(cmd).Next(Config.CooldownAnswers.Count)];
                ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: {1}", args.SenderNickname, cdAnswer);
                return(true);
            }

            return(false);
        }
예제 #2
0
        protected virtual void HandleRegexCountCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            var    counterName = (string)cmd.Arguments[0];
            var    nick        = (string)cmd.Arguments[1];
            string regexString = ((string)cmd.Arguments[2]).Trim();

            Counter counter = Config.Counters.FirstOrDefault(c => c.CommandName == counterName);

            if (counter == null)
            {
                ConnectionManager.SendChannelMessage(msg.Channel, $"{msg.SenderNickname}: Unknown counter '{counterName}'");
                return;
            }

            Regex regex;

            try
            {
                regex = new Regex(regexString, RegexOptions.Compiled);
            }
            catch (ArgumentException ae)
            {
                ConnectionManager.SendChannelMessage(msg.Channel, $"{msg.SenderNickname}: Invalid regex: {ae.Message}");
                return;
            }

            TryToMatch(counter, msg.Channel, msg.SenderNickname, messageSubstring: null, messageRegex: regex);
        }
예제 #3
0
        protected virtual void HandleProverbCommand(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            var client = new HttpClient
            {
                Timeout = TimeSpan.FromSeconds(Config.TimeoutSeconds)
            };

            using (var request = new HttpRequestMessage(HttpMethod.Get, Config.ProverbURI))
            {
                var htmlDoc = new HtmlDocument();

                using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).SyncWait())
                    using (Stream responseStream = response.Content.ReadAsStreamAsync().SyncWait())
                    {
                        htmlDoc.Load(responseStream);
                    }

                string proverb = htmlDoc.DocumentNode
                                 .SelectSingleNode(Config.NodeSelector)
                                 ?.InnerText;

                if (proverb != null)
                {
                    ConnectionManager.SendChannelMessage(args.Channel, $"{args.SenderNickname}: {proverb}");
                }
            }
        }
예제 #4
0
 private void HandleChannelAka([NotNull] CommandMatch cmd, [NotNull] IChannelMessageEventArgs e)
 {
     ActuallyHandleChannelOrQueryAka(
         cmd, e,
         txt => ConnectionManager.SendChannelMessage(e.Channel, $"{e.SenderNickname}: {txt}")
         );
 }
예제 #5
0
        protected virtual void HandleTopGratefulCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            List <NicknameAndCount> top;

            using (var ctx = GetNewContext())
            {
                top = ctx.ThanksEntries
                      .Where(te => !te.Deleted)
                      .GroupBy(te => te.ThankerLowercase, (thanker, thanksEntries) => new NicknameAndCount
                {
                    Nickname = thanker,
                    Count    = thanksEntries.Count()
                })
                      .OrderByDescending(teg => teg.Count)
                      .Take(Config.MostThankedCount)
                      .ToList()
                ;
            }

            ConnectionManager.SendChannelMessageFormat(
                msg.Channel,
                "{0}: {1}",
                msg.SenderNickname,
                top.Select(NicknameAndCountString).StringJoin(", ")
                );
        }
예제 #6
0
        protected virtual void HandleTelCommand(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            string telNumber = ((string)cmd.Arguments[0]);

            if (telNumber.Length > 0)
            {
                // remove leading space
                telNumber = telNumber.Substring(1);
            }
            var ret = new StringBuilder(telNumber.Length);

            foreach (char c in telNumber)
            {
                char target;
                if (TelNoDictionary.TryGetValue(c, out target))
                {
                    ret.Append(target);
                }
                else
                {
                    ret.Append(c);
                }
            }

            ConnectionManager.SendChannelMessage(args.Channel, ret.ToString());
        }
예제 #7
0
        protected virtual void HandlePassCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            if (msg.Channel != Config.UnoChannel)
            {
                return;
            }

            lock (TurnLock)
            {
                if (CurrentPlayer.Nick != msg.SenderNickname)
                {
                    ConnectionManager.SendChannelMessageFormat(
                        Config.UnoChannel,
                        "It's not your turn, {0}.",
                        msg.SenderNickname
                        );
                    return;
                }

                if (!DrewLast)
                {
                    ConnectionManager.SendChannelMessageFormat(
                        Config.UnoChannel,
                        "You have to draw first, {0}.",
                        msg.SenderNickname
                        );
                    return;
                }

                // skip to the next player
                AdvanceToNextPlayer();
            }
        }
예제 #8
0
        protected virtual void HandleChannelMessage(object sender, IChannelMessageEventArgs args, MessageFlags flags)
        {
            if (Config.CooldownUpperBoundary < 0)
            {
                // the cooldown feature is not being used
                return;
            }

            CooldownState cdState;

            if (!ChannelToCooldown.TryGetValue(args.Channel, out cdState))
            {
                cdState = new CooldownState();
                ChannelToCooldown[args.Channel] = cdState;
            }

            if (cdState.CooldownValue > 0)
            {
                --cdState.CooldownValue;
                if (cdState.CooldownValue == 0)
                {
                    cdState.CooldownTriggered = false;
                }
            }
        }
        protected virtual void HandleBaseNickCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            string channel   = msg.Channel;
            string requestor = msg.SenderNickname;

            var whichNick = (string)cmd.Arguments[0];

            using (var ctx = GetNewContext())
            {
                var baseNick = FindBaseNickFor(whichNick, ctx);
                if (baseNick == null)
                {
                    ConnectionManager.SendChannelMessage(
                        channel,
                        $"{requestor}: I can't find the nickname {whichNick}."
                        );
                }
                else
                {
                    ConnectionManager.SendChannelMessage(
                        channel,
                        $"{requestor}: The base nickname for {whichNick} is {baseNick}."
                        );
                }
            }
        }
예제 #10
0
        protected virtual void HandleIKnewThatChannelCommand(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            string senderLower = (ConnectionManager.RegisteredNameForNick(args.SenderNickname) ?? args.SenderNickname)
                                 .ToLowerInvariant();
            string keywordLower = ((string)cmd.Arguments[0]).ToLowerInvariant();

            using (var ctx = GetNewContext())
            {
                IKnewThatEntry matchingEntry = ctx.Entries
                                               .FirstOrDefault(e => e.AuthorLowercase == senderLower && e.KeywordLowercase == keywordLower);

                if (matchingEntry == null)
                {
                    ConnectionManager.SendChannelMessage(
                        args.Channel,
                        $"{args.SenderNickname}: No, you didn't!"
                        );
                    return;
                }

                DateTimeOffset timestampLocal = matchingEntry.Timestamp.ToLocalTime();

                ConnectionManager.SendChannelMessage(
                    args.Channel,
                    $"I confirm that on {timestampLocal:yyyy-MM-dd} at {timestampLocal:HH:mm:ss}, {args.SenderNickname} knew the following: {matchingEntry.Message}"
                    );

                ctx.Entries.Remove(matchingEntry);
                ctx.SaveChanges();
            }
        }
예제 #11
0
        protected virtual void HandleAddQuoteCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            string normalizedNick = ConnectionManager.RegisteredNameForNick(msg.SenderNickname) ?? msg.SenderNickname;

            using (var ctx = GetNewContext())
            {
                var newFreeFormQuote = new Quote
                {
                    Timestamp   = DateTime.Now.ToUniversalTimeForDatabase(),
                    Channel     = msg.Channel,
                    Author      = normalizedNick,
                    MessageType = "F",
                    Body        = (string)cmd.Arguments[0]
                };
                ctx.Quotes.Add(newFreeFormQuote);
                ctx.SaveChanges();
                LastQuoteIDs[msg.Channel] = newFreeFormQuote.ID;
            }
            ConnectionManager.SendChannelMessage(
                msg.Channel,
                "Done."
                );

            // invalidate these
            ShuffledAnyQuotes  = null;
            ShuffledBadQuotes  = null;
            ShuffledGoodQuotes = null;
        }
예제 #12
0
        protected virtual void HandleBotTestCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            if (msg.Channel != Config.UnoChannel)
            {
                return;
            }

            lock (TurnLock)
            {
                BotTestCount = (long)cmd.Arguments[0];

                ConnectionManager.SendChannelMessageFormat(
                    Config.UnoChannel,
                    "{0} engaged bot test mode; {1} games left!",
                    msg.SenderNickname,
                    BotTestCount
                    );

                // prepare a game
                PrepareGame();

                // trigger bot joinage
                ConnectionManager.SendChannelMessage(Config.UnoChannel, "?join");

                // wait for bot joinage
                BotTestJoinRequested = DateTime.UtcNow;
            }
        }
예제 #13
0
 private void HandleWDYTICommandInChannel(CommandMatch cmd, IChannelMessageEventArgs msg)
 {
     HandleMessage(
         (string)cmd.Arguments[0],
         body => ConnectionManager.SendChannelMessage(msg.Channel, $"{msg.SenderNickname}: {body}")
         );
 }
예제 #14
0
        protected virtual void HandleAnyChannelMessage(object sender, IChannelMessageEventArgs e, MessageFlags flags)
        {
            if (!Config.ChannelsPatterns.ContainsKey(e.Channel))
            {
                // don't police this channel
                return;
            }

            IEnumerable <PuntPattern> relevantPatterns = Config.CommonPatterns;

            IEnumerable <PuntPattern> channelPatterns = Config.ChannelsPatterns[e.Channel];

            if (channelPatterns != null)
            {
                relevantPatterns = relevantPatterns.Concat(channelPatterns);
            }

            string normalizedNick = ConnectionManager.RegisteredNameForNick(e.SenderNickname) ?? e.SenderNickname;

            foreach (PuntPattern pattern in relevantPatterns)
            {
                if (!AnyMatch(normalizedNick, pattern.NickPatterns))
                {
                    // wrong user
                    continue;
                }

                if (AnyMatch(normalizedNick, pattern.NickExceptPatterns))
                {
                    // whitelisted user
                    continue;
                }

                if (pattern.ChancePercent.HasValue)
                {
                    int val = Randomizer.Next(100);
                    if (val >= pattern.ChancePercent.Value)
                    {
                        // luck is on their side
                        continue;
                    }
                }

                if (!AnyMatch(e.Message, pattern.BodyPatterns))
                {
                    // no body match
                    continue;
                }

                if (AnyMatch(e.Message, pattern.BodyExceptPatterns))
                {
                    // body exception
                    continue;
                }

                // match! kick 'em!
                ConnectionManager.KickChannelUser(e.Channel, e.SenderNickname, pattern.KickMessage);
                return;
            }
        }
예제 #15
0
        protected virtual void HandleDealCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            if (msg.Channel != Config.UnoChannel)
            {
                return;
            }

            lock (TurnLock)
            {
                switch (CurrentGameState)
                {
                case GameState.NoGame:
                    Logger.LogDebug("{Nickname} is trying to deal no game", msg.SenderNickname);
                    return;

                default:
                    Logger.LogError("invalid game state when trying to add player to game");
                    return;

                case GameState.Preparation:
                case GameState.InProgress:
                    // continue below
                    break;
                }

                DealGame();
            }
        }
예제 #16
0
        protected virtual void HandleChannelMessageOrAction(object sender, IChannelMessageEventArgs e, MessageFlags flags)
        {
            BounceCriterion crit = InterestingCriterion(e.SenderNickname, channel: null);

            if (crit != null && crit.ForgetOnChannelMessage)
            {
                RelevantJoins.RemoveAll(j => j.Nickname == e.SenderNickname);
            }
        }
예제 #17
0
        protected virtual void HandleNewCommand(CommandMatch cmd, IChannelMessageEventArgs message)
        {
            if (!EnsureOp(message))
            {
                return;
            }

            var    criterionName        = (string)cmd.Arguments[0];
            string detectionRegexString = ((string)cmd.Arguments[1]).Trim();

            try
            {
                RegexCache.GetOrAdd(detectionRegexString);
            }
            catch (ArgumentException)
            {
                ConnectionManager.SendChannelMessage(message.Channel, $"{message.SenderNickname}: Invalid regular expression.");
                return;
            }

            using (var ctx = GetNewContext())
            {
                // see if a criterion already matches
                Criterion crit = ctx.Criteria
                                 .FirstOrDefault(c => c.Name == criterionName && c.Channel == message.Channel);
                if (crit == null)
                {
                    // create a new criterion
                    crit = new Criterion
                    {
                        Name           = criterionName,
                        Channel        = message.Channel,
                        DetectionRegex = detectionRegexString,
                        Enabled        = true
                    };
                }
                else if (crit.Enabled)
                {
                    ConnectionManager.SendChannelMessage(
                        message.Channel,
                        $"{message.SenderNickname}: That criterion name is already in use."
                        );
                    return;
                }
                else
                {
                    // modify the existing criterion and re-enable it
                    crit.DetectionRegex = detectionRegexString;
                    crit.Enabled        = true;
                }
                ctx.SaveChanges();

                // update the cache
                Dictionary <string, long> commandsIDs = ObtainCommandCacheForChannel(message.Channel);
                commandsIDs[crit.Name] = crit.ID;
            }
        }
예제 #18
0
        protected virtual void HandleStopTriviaCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            if (msg.Channel != Config.TriviaChannel)
            {
                return;
            }

            StopGame();
        }
예제 #19
0
        protected void HandleAutoLinkInfoCommand(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            var senderUsername = ConnectionManager.RegisteredNameForNick(args.SenderNickname);

            if (senderUsername == null)
            {
                ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: You must be registered to use this feature.", args.SenderNickname);
                return;
            }

            bool removeSubscription = (cmd.CommandName == "noautolinkinfo");

            using (var ctx = GetNewContext())
            {
                var currentSub = ctx.OptedInUsers.FirstOrDefault(u => u.UserName == senderUsername);

                if (removeSubscription)
                {
                    if (currentSub == null)
                    {
                        ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: You are not subscribed to auto link info.", args.SenderNickname);
                    }
                    else
                    {
                        Logger.LogInformation(
                            "{Nickname} ({Username}) is unsubscribing from auto link info",
                            args.SenderNickname, senderUsername
                            );

                        ctx.OptedInUsers.Remove(currentSub);
                        ctx.SaveChanges();
                        ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: You have been unsubscribed from auto link info.", args.SenderNickname);
                    }
                }
                else
                {
                    // add subscription
                    if (currentSub == null)
                    {
                        Logger.LogInformation(
                            "{Nickname} ({Username}) is subscribing to auto link info",
                            args.SenderNickname, senderUsername
                            );

                        ctx.OptedInUsers.Add(new OptedInUser {
                            UserName = senderUsername
                        });
                        ctx.SaveChanges();
                        ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: You are now subscribed to auto link info.", args.SenderNickname);
                    }
                    else
                    {
                        ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: You are already subscribed to auto link info.", args.SenderNickname);
                    }
                }
            }
        }
예제 #20
0
        protected virtual void HandleChannelAction(object sender, IChannelMessageEventArgs e, MessageFlags flags)
        {
            if (flags.HasFlag(MessageFlags.UserBanned))
            {
                return;
            }

            // remember
            RememberMessage(e.Channel, new LastMessage(LastMessageType.ChannelAction, e.SenderNickname, e.Message));
        }
예제 #21
0
 protected virtual void LinksAction(IChannelMessageEventArgs args, MessageFlags flags, IList <Uri> links)
 {
     // respond?
     if (Config.AutoShowLinkInfo || args.Message.StartsWith(LinkCommandPrefix))
     {
         foreach (var linkAndInfo in links.Select(ObtainLinkInfo))
         {
             PostLinkInfoToChannel(linkAndInfo, args.Channel);
         }
     }
 }
예제 #22
0
        protected virtual void HandleChannelMessage(object sender, IChannelMessageEventArgs args, MessageFlags flags)
        {
            if (!Config.Channels.Contains(args.Channel))
            {
                // wrong channel
                return;
            }

            ProcessPotentialHighlight(args);
            ProcessPendingRetributions();
        }
예제 #23
0
        protected virtual void HandleYesNoCommand(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            if (CheckAndHandleCooldown(cmd, args))
            {
                return;
            }

            string yesNoAnswer = Config.YesNoAnswers[ChosenRNG(cmd).Next(Config.YesNoAnswers.Count)];

            ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: {1}", args.SenderNickname, yesNoAnswer);
        }
예제 #24
0
        protected virtual void HandleRollCommand(CommandMatch cmd, IChannelMessageEventArgs args)
        {
            var rolls      = (List <Match>)cmd.Arguments[0];
            var diceGroups = new List <DiceGroup>();

            foreach (Match rollMatch in rolls)
            {
                var diceGroup = ObtainDiceGroup(rollMatch, args.Channel, args.SenderNickname);
                if (diceGroup == null)
                {
                    // error occurred and reported; bail out
                    return;
                }
                diceGroups.Add(diceGroup);
            }

            if (diceGroups.Count > Config.MaxRollCount)
            {
                ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: Too many rolls.", args.SenderNickname);
                return;
            }

            // special-case 2d1
            if (diceGroups.Count == 1 && diceGroups[0].DieCount == 2 && diceGroups[0].SideCount == 1 && diceGroups[0].AddValue == 0)
            {
                ConnectionManager.SendChannelAction(args.Channel, "rolls its eyes");
                return;
            }

            Random rng      = ChosenRNG(cmd);
            var    allRolls = new List <string>();

            foreach (var diceGroup in diceGroups)
            {
                var theseRolls = new List <string>(diceGroup.DieCount);
                for (int i = 0; i < diceGroup.DieCount; ++i)
                {
                    if (diceGroup.SideCount == 1 && Config.ObstinateAnswers.Count > 0)
                    {
                        // special case: give an obstinate answer instead since a 1-sided toss has an obvious result
                        string obstinateAnswer = Config.ObstinateAnswers[rng.Next(Config.ObstinateAnswers.Count)];
                        theseRolls.Add(obstinateAnswer);
                    }
                    else
                    {
                        long roll = rng.Next(diceGroup.SideCount) + 1 + diceGroup.AddValue;
                        theseRolls.Add(roll.ToString(CultureInfo.InvariantCulture));
                    }
                }
                allRolls.Add(theseRolls.StringJoin(" "));
            }

            ConnectionManager.SendChannelMessageFormat(args.Channel, "{0}: {1}", args.SenderNickname, allRolls.StringJoin("; "));
        }
예제 #25
0
        protected virtual void HandleWeatherCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            string location = ((string)cmd.Arguments[0]).Trim();

            if (location.Length == 0)
            {
                location = Config.DefaultLocation;
            }

            GetWeatherForLocation(location, msg.Channel, msg.SenderNickname, showLocName: cmd.CommandName == "weather");
        }
예제 #26
0
        protected virtual void HandleLeaveCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            if (msg.Channel != Config.UnoChannel)
            {
                return;
            }

            lock (TurnLock)
            {
                RemovePlayerFromGame(msg.SenderNickname);
            }
        }
예제 #27
0
        protected virtual bool EnsureOp(IChannelMessageEventArgs message)
        {
            ChannelUserLevel level = ConnectionManager.GetChannelLevelForUser(message.Channel, message.SenderNickname);

            if (level < ChannelUserLevel.HalfOp)
            {
                ConnectionManager.SendChannelMessage(message.Channel, $"{message.SenderNickname}: You need to be a channel operator.");
                return(false);
            }

            return(true);
        }
예제 #28
0
        protected virtual void HandleStartTriviaCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            if (msg.Channel != Config.TriviaChannel)
            {
                return;
            }

            if (GameState == null)
            {
                StartGame();
            }
        }
예제 #29
0
        protected virtual void HandleChannelMessage(object sender, IChannelMessageEventArgs args, MessageFlags flags)
        {
            if (flags.HasFlag(MessageFlags.UserBanned))
            {
                return;
            }

            if (args.Channel != Config.CasinoChannel)
            {
                return;
            }

            bool botJoin = false;

            if (args.Message.Trim() == "?join")
            {
                // "?join"
                botJoin = true;
            }
            else
            {
                string[] bits = args.Message.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                if (
                    bits.Length >= 2 &&
                    bits[0] == "?join" &&
                    bits.Skip(1).Any(b => b.Equals(ConnectionManager.MyNickname, StringComparison.OrdinalIgnoreCase))
                    )
                {
                    // "?join MyBot" or "?join ThisBot ThatBot MyBot"
                    botJoin = true;
                }
            }

            if (botJoin)
            {
                ConnectionManager.SendChannelMessage(args.Channel, ".botjoin");
                ConnectionManager.SendChannelMessage(args.Channel, ".grules");
            }

            // FIXME: these should be JSON events
            if (string.Equals(args.SenderNickname, Config.GameMasterNickname, StringComparison.OrdinalIgnoreCase))
            {
                if (
                    args.Message == "Merging the discards back into the shoe and shuffling..." ||
                    args.Message == "The dealer's shoe has been shuffled."
                    )
                {
                    CardCounter.ShoeShuffled();
                    DispatchStratDebugMessage($"shoe shuffled -> {CardCounter}");
                }
            }
        }
예제 #30
0
        protected virtual void HandleIntervalCommand(CommandMatch cmd, IChannelMessageEventArgs msg)
        {
            var      dateTimeString = (string)cmd.Arguments[0];
            DateTime?timestamp      = TimeUtil.DateTimeFromString(dateTimeString);

            if (!timestamp.HasValue)
            {
                return;
            }

            DateTime timestampUTC      = timestamp.Value.ToUniversalTime();
            DateTime nowUTC            = DateTime.UtcNow;
            DateTime nowUTCFullSeconds = nowUTC.AddTicks(-(nowUTC.Ticks % TicksPerSecond));

            CalendarTimeSpan diff = TimeComparison.CalendarDifference(nowUTCFullSeconds, timestampUTC);

            var pieces = new List <string>();

            MaybeAddUnit(pieces, diff.Years, "year", "years");
            MaybeAddUnit(pieces, diff.Months, "month", "months");
            MaybeAddUnit(pieces, diff.Days, "day", "days");
            MaybeAddUnit(pieces, diff.Hours, "hour", "hours");
            MaybeAddUnit(pieces, diff.Minutes, "minute", "minutes");
            MaybeAddUnit(pieces, diff.Seconds, "second", "seconds");

            string message;

            if (pieces.Count == 0)
            {
                message = "That’s now!";
            }
            else
            {
                var messageBuilder = new StringBuilder();

                if (pieces.Count > 1)
                {
                    messageBuilder.Append(string.Join(", ", pieces.Take(pieces.Count - 1)));
                    // 1 year, 2 months, 3 days[ and 4 hours]

                    messageBuilder.Append(" and ");
                    // 1 year, 2 months, 3 days and [4 hours]
                }
                messageBuilder.Append(pieces.Last());
                messageBuilder.Append(diff.Negative ? " ago." : " remaining.");

                message = messageBuilder.ToString();
            }

            ConnectionManager.SendChannelMessage(msg.Channel, $"{msg.SenderNickname}: {message}");
        }