Beispiel #1
0
        public static string GetMessage(RecentChange change, string format, string lang)
        {
            // Parse length of the diff
            string strLength = "";
            int    length    = (change.LengthNew - change.LengthOld);

            strLength = length.ToString();
            if (length > 0)
            {
                strLength = "+" + strLength;
            }
            strLength = $"({strLength})";

            if (length > 500 || length < -500)
            {
                strLength = $"**{strLength}**";
            }

            // Markdownify link
            string link = format.Replace("/wiki/$1", string.Format("/?{0}{1}", (change.OldID != 0 ? "diff=" : "oldid="), change.RevID));

            link = string.Format("([{0}]({1}))", Locale.GetMessage("eventstreams-diff", lang), link);

            // Markdownify user
            string user     = "******" + change.User;
            string talk     = "User_talk:" + change.User;
            string contribs = "Special:Contributions/" + change.User;

            user     = Linking.GetLink(user, format, true);
            talk     = Linking.GetLink(talk, format, true);
            contribs = Linking.GetLink(contribs, format, true);

            talk = string.Format("[{0}]({1})", Locale.GetMessage("eventstreams-talk", lang), talk);

            IPAddress address;

            if (IPAddress.TryParse(change.User, out address))
            {
                user = $"[{change.User}]({contribs}) ({talk})";
            }
            else
            {
                contribs = string.Format("[{0}]({1})", Locale.GetMessage("eventstreams-contribs", lang), contribs);
                user     = $"[{change.User}]({user}) ({talk} | {contribs})";
            }

            // Parse comment, adjusting for its length
            string comment = ParseComment(change.Summary, format);
            string msg     = $"{link} . . {strLength} . . {user}";

            if (msg.Length + comment.Length > 2000)
            {
                comment = ParseComment(change.Summary, format, false);
            }
            msg += comment;

            return(msg);
        }
Beispiel #2
0
        public async Task SetWiki(CommandContext ctx,
                                  [Description("configuring-help-wiki-value"), RemainingText] string value)
        {
            string lang = Config.GetLang(ctx.Guild.Id.ToString());

            // Ensure that we are in private channel
            if (ctx.Channel.Name != "moderators")
            {
                await ctx.RespondAsync(Locale.GetMessage("denied", lang));

                return;
            }
            ;
            await ctx.TriggerTypingAsync();

            // Check for return to default
            if (value == "-")
            {
                value = Config.GetWiki();
            }

            // Check for required parameters
            if (value.ToString() == "")
            {
                await ctx.RespondAsync(Locale.GetMessage("configuring-required-value", lang, "help guildWiki", Config.GetValue("prefix")));

                return;
            }

            if (!value.Contains("/wiki/$1"))
            {
                await ctx.RespondAsync(Locale.GetMessage("configuring-badvalue-wiki", lang));
            }

            // Provide some changes
            value = value.Replace("<", String.Empty).Replace(">", String.Empty);

            // Do action and respond
            int succeeds = Config.SetOverride(ctx.Guild.Id.ToString(), "wiki", value);

            if (succeeds == Config.RESULT_CHANGE)
            {
                Linking.Init(ctx.Guild.Id.ToString());
                await ctx.RespondAsync(Locale.GetMessage("configuring-changed-wiki", lang, value));
            }
            if (succeeds == Config.RESULT_RESET)
            {
                Linking.Remove(ctx.Guild.Id.ToString());
            }
            await RespondOnErrors(succeeds, ctx, lang);
        }
Beispiel #3
0
        public static DiscordEmbedBuilder GetEmbed(RecentChange change, string format, string lang)
        {
            DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
                                        .WithTimestamp(change.Timestamp);

            DiscordColor embedColor = new DiscordColor(0x72777d);
            string       embedIcon  = "2/25/MobileFrontend_bytes-neutral.svg/512px-MobileFrontend_bytes-neutral.svg.png";

            // Parse statuses from the diff
            string status = "";

            if (change.Type == RecentChange.ChangeType.New)
            {
                status += Locale.GetMessage("eventstreams-new", lang);
            }
            if (change.Minor == true)
            {
                status += Locale.GetMessage("eventstreams-minor", lang);
            }

            if (status != "")
            {
                embed.WithFooter(status);
            }

            // Parse length of the diff
            int length = (change.LengthNew - change.LengthOld);

            if (length > 0)
            {
                embedColor = new DiscordColor(0x00af89);
                embedIcon  = "a/ab/MobileFrontend_bytes-added.svg/512px-MobileFrontend_bytes-added.svg.png";
            }
            else if (length < 0)
            {
                embedIcon  = "7/7c/MobileFrontend_bytes-removed.svg/512px-MobileFrontend_bytes-removed.svg.png";
                embedColor = new DiscordColor(0xdd3333);
            }

            embed
            .WithAuthor(
                change.Title,
                Linking.GetLink(change.Title, format, true),
                string.Format("https://upload.wikimedia.org/wikipedia/commons/thumb/{0}", embedIcon)
                )
            .WithColor(embedColor)
            .WithDescription(GetMessage(change, format, lang));

            return(embed);
        }
        public async Task SetChannelWiki(CommandContext ctx,
                                         [Description("configuring-help-wiki-value"), RemainingText] string value)
        {
            string lang = Config.GetLang(ctx.Guild.Id.ToString());
            await ctx.TriggerTypingAsync();

            // Check for required parameters
            if (value.ToString() == "")
            {
                await ctx.RespondAsync(Locale.GetMessage("configuring-required-value", lang, ctx.Command.Name, Config.GetValue("prefix")));

                return;
            }

            if (value != "-" && !value.Contains("/wiki/$1"))
            {
                await ctx.RespondAsync(Locale.GetMessage("configuring-badvalue-wiki", lang, "/wiki/$1"));

                return;
            }

            // Provide some changes
            value = value.Replace("<", String.Empty).Replace(">", String.Empty);

            // Reset to default server value if necessary
            if (value == Config.GetWiki(ctx.Guild.Id.ToString()))
            {
                value = "-";
            }

            // Do action and respond
            int succeeds = Config.SetOverride($"#{ctx.Channel.Id.ToString()}", "wiki", value);

            if (succeeds == Config.RESULT_CHANGE)
            {
                Linking.Init($"#{ctx.Channel.Id.ToString()}");
                await ctx.RespondAsync(Locale.GetMessage("configuring-changed-wiki-channel", lang, value));
            }
            if (succeeds == Config.RESULT_RESET)
            {
                Linking.Remove($"#{ctx.Channel.Id.ToString()}");
            }
            await RespondOnErrors(succeeds, ctx, lang);
        }
Beispiel #5
0
        private static string ParseComment(string summary, string format, bool linkify = true)
        {
            if (summary.Length == 0)
            {
                return("");
            }

            string linkPattern     = "\\[{2}([^\\[\\]\\|\n]+)\\]{2}";
            string linkPatternPipe = "\\[{2}([^\\[\\]\\|\n]+)\\|([^\\[\\]\n]+)\\]{2}";

            // Transform code for section to simpler version
            string comment = summary.ToString().Replace("/* ", "→");

            comment = Regex.Replace(comment, " \\*/$", string.Empty).Replace(" */", ":");

            if (linkify)
            {
                // Linkify every wiki link in comment text
                comment = Regex.Replace(comment, linkPattern, m => {
                    string title = m.Groups[1].Value;
                    string link  = string.Format("[{0}]({1})", title, Linking.GetLink(title, format, true));

                    return(link);
                });

                comment = Regex.Replace(comment, linkPatternPipe, m => {
                    string title = m.Groups[1].Value;
                    string text  = m.Groups[2].Value;
                    string link  = string.Format("[{0}]({1})", text, Linking.GetLink(title, format, true));

                    return(link);
                });
            }
            else
            {
                // Display wiki links as plain text
                comment = Regex.Replace(comment, linkPattern, "$1");
                comment = Regex.Replace(comment, linkPatternPipe, "$2");
            }

            // Add italic and parentheses
            comment = $" *({comment})*";
            return(comment);
        }
Beispiel #6
0
        /// <summary>
        /// Common checks for streaming commands.
        /// </summary>
        /// <param name="ctx">Discord information.</param>
        /// <param name="channel">Discord channel.</param>
        /// <param name="args">Stream arguments.</param>
        /// <param name="callback">Method to execute after all checks.</param>
        private async Task CommandChecks(CommandContext ctx, DiscordChannel channel, string args, Action <Dictionary <string, dynamic>, string> callback)
        {
            string lang = Config.GetLang(ctx.Guild.Id.ToString());
            Dictionary <string, dynamic> arguments = ParseArguments(args);
            await ctx.TriggerTypingAsync();

            // Check for the goal
            if (
                args == "" ||
                !(arguments.ContainsKey("title") || arguments.ContainsKey("namespace")) ||
                arguments.ContainsKey("title") && Linking.IsInvalid(arguments["title"])
                )
            {
                await ctx.RespondAsync(Locale.GetMessage("streaming-required-goal", lang, ctx.Command.Name, Config.GetValue("prefix")));

                return;
            }

            callback(arguments, lang);
        }
Beispiel #7
0
        /// <summary>
        /// Initialise the common functions for every server.
        /// </summary>
        /// <param name="e">Discord event information.</param>
        private Task Client_GuildAvailable(DiscordClient sender, GuildCreateEventArgs e)
        {
            // Log the name of the guild that just became available
            LogMessage($"Server is loaded: {e.Guild.Name}");

            // Load custom values if needed
            string guild = e.Guild.Id.ToString();

            Task.Run(async() =>
            {
                Linking.Init(guild);

                Locale.Init(Config.GetLang(guild));

                if (Config.GetTWChannel(guild) != null && Config.GetTWLang(guild) != null)
                {
                    TranslateWiki.Init(Config.GetTWChannel(guild), Config.GetTWLang(guild));
                }
            });

            return(Task.CompletedTask);
        }
Beispiel #8
0
        private Task Client_GuildAvailable(GuildCreateEventArgs e)
        {
            // Log the name of the guild that just became available
            e.Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", $"Guild available: {e.Guild.Name}", DateTime.Now);

            // Load custom values if needed
            string guild = e.Guild.Id.ToString();

            Linking.Init(guild);

            Locale.LoadCustomLocale(Config.GetLang(guild));

            if (Config.GetTWChannel() != null && Config.GetTWLang() != null)
            {
                TranslateWiki.Init(Config.GetTWChannel(guild), Config.GetTWLang(guild));
            }

            if (Config.GetDomain() != "")
            {
                EventStreams.Subscribe(Config.GetDomain(guild));
            }

            return(Task.FromResult(0));
        }
Beispiel #9
0
        /// <summary>
        /// Initialise the bot and keep it running
        /// </summary>
        public async Task Run()
        {
            // Set proper TLS settings
            ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

            // Check for a token
            string tokenPath = @"token.txt";

            if (!File.Exists(tokenPath))
            {
                Console.WriteLine("Please create a file called \"token.txt\" before running the bot!");
                Console.WriteLine("[Press any key to exit...]");
                Console.ReadKey();
                Environment.Exit(0);
            }
            Token = File.ReadAllText(tokenPath, Encoding.Default);

            // Get JSON config file
            Config.Init();

            // Initialise Discord client
            Client = new DiscordClient(new DiscordConfiguration()
            {
                AutoReconnect   = true,
                LargeThreshold  = 250,
                MinimumLogLevel = LogLevel.Information,
                Token           = Token,
                TokenType       = TokenType.Bot,
            });

            // Initialise events
            LogMessage($"DiscordWikiBot, version {Version}");

            // Get default locale
            Locale.Init();

            // Get site information and start linking bot
            LogMessage("Getting wiki site information");
            Linking.Init();

            // Methods for linking bot
            Client.MessageCreated += (s, e) =>
            {
                Task.Run(async() =>
                {
                    await Linking.Answer(s, e);
                });

                return(Task.CompletedTask);
            };
            Client.MessageUpdated += (s, e) =>
            {
                Task.Run(async() =>
                {
                    await Linking.Edit(s, e);
                });

                return(Task.CompletedTask);
            };
            Client.MessageDeleted      += Linking.Delete;
            Client.MessagesBulkDeleted += Linking.BulkDelete;

            // Start EventStreams
            if (Config.GetDomain() != null)
            {
                EventStreams.Init();
            }

            // Start Translatewiki fetches
            if (Config.GetTWChannel() != null && Config.GetTWLang() != null)
            {
                TranslateWiki.Init();
            }

            // Set some events for logging the information
            Client.Ready          += Client_Ready;
            Client.GuildAvailable += Client_GuildAvailable;
            Client.GuildCreated   += Client_GuildCreated;
            Client.GuildDeleted   += Client_GuildDeleted;
            Client.ClientErrored  += Client_ClientErrored;

            // Initialise commands
            LogMessage("Setting up commands");
            Commands = Client.UseCommandsNext(new CommandsNextConfiguration
            {
                StringPrefixes      = new[] { Config.GetValue("prefix") },
                EnableDms           = false,
                EnableMentionPrefix = true,
            });

            Commands.RegisterCommands <Pinging>();

            Commands.RegisterCommands <Configuring>();

            if (EventStreams.Enabled)
            {
                Commands.RegisterCommands <Streaming>();
            }

            // Set up custom formatter
            Commands.SetHelpFormatter <LocalisedHelpFormatter>();

            // Connect and start
            LogMessage("Connecting...");
            await Client.ConnectAsync();

            // Make sure not to close down automatically
            await CtrlC();
        }
Beispiel #10
0
        public static async Task React(JToken[] list, string lang)
        {
            // List of MediaWiki/Wikimedia-related projects
            string[] twProjects = new string[]
            {
                "8",                 // MediaWiki
                "1206",              // Wikimedia
                "1238",              // Pywikibot
                "1244",              // Kiwix
                "1248",              // Huggle
                "1274",              // Phabricator
            };

            Dictionary <string, string> twNames = new Dictionary <string, string>()
            {
                { "8", "MediaWiki" },
                { "1206", "Wikimedia" },
                { "1238", "Pywikibot" },
                { "1244", "Kiwix" },
                { "1248", "Huggle" },
                { "1274", "Phabricator" },
            };

            // Filter only translations from projects above
            bool          gotToLatest = false;
            List <JToken> query       = list.Where(jt => jt.Type == JTokenType.Object).Select(item =>
            {
                string key = item["key"].ToString();
                if (twProjects.Where(x => key.StartsWith(x + ":")).ToList().Count > 0)
                {
                    // Check if matches with latest fetch
                    if (key == LatestFetchKey[lang])
                    {
                        gotToLatest = true;
                    }

                    // Return if no there is still no match
                    if (gotToLatest == false)
                    {
                        return(item);
                    }
                }

                return(null);
            }).Where(i => i != null).ToList();

            if (query.Count == 0)
            {
                return;
            }

            // Fetch every author for future message
            int count = query.Count;
            Dictionary <string, List <string> > authors = new Dictionary <string, List <string> >();
            List <string> allAuthors = new List <string>();

            foreach (var item in query)
            {
                string key        = item["key"].ToString();
                string ns         = twProjects.Where(x => key.StartsWith(x + ":")).ToList().First().ToString();
                string translator = item["properties"]["last-translator-text"].ToString();

                if (!authors.ContainsKey(ns))
                {
                    authors[ns] = new List <string>();
                }
                if (!authors[ns].Contains(translator))
                {
                    authors[ns].Add(translator);
                }
                if (!allAuthors.Contains(translator))
                {
                    allAuthors.Add(translator);
                }
            }

            // Send Discord messages to guilds
            DiscordClient client = Program.Client;

            foreach (string chan in Channels[lang])
            {
                DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
                                            .WithColor(new DiscordColor(0x013467))
                                            .WithFooter("translatewiki.net");

                // Fetch info about channel
                ulong          chanId  = ulong.Parse(chan);
                DiscordChannel channel = await client.GetChannelAsync(chanId);

                string guildLang = Config.GetLang(channel.GuildId.ToString());

                // Remember the key of first message
                Config.SetInternal(channel.Id.ToString(), "translatewiki-key", query[0]["key"].ToString());

                // Inform about deadline if needed
                string deadlineInfo = null;
                if (UpdateDeadline)
                {
                    deadlineInfo       = Locale.GetMessage("translatewiki-deadline", guildLang);
                    UpdateDeadline     = false;
                    UpdateDeadlineDone = true;
                }

                // Build messages
                string headerCount = (gotToLatest ? count.ToString() : count.ToString() + "+");
                string header      = Locale.GetMessage("translatewiki-header", guildLang, headerCount, count, allAuthors.Count);
                embed.WithAuthor(
                    header,
                    string.Format("https://translatewiki.net/wiki/Special:RecentChanges?translations=only&namespace={0}&limit=500&trailer=/ru", string.Join("%3B", twProjects)),
                    "https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/Translatewiki.net_logo.svg/512px-Translatewiki.net_logo.svg.png"
                    );

                // Check if authors list doesn’t exceed
                string desc = string.Join("\n", authors.Select(ns => {
                    string str = twNames[ns.Key] + ": ";
                    str       += string.Join(", ", ns.Value.Select(author =>
                    {
                        return(string.Format("[{0}]({1})", author, Linking.GetLink(author, "https://translatewiki.net/wiki/Special:Contribs/$1", true)));
                    }));

                    return(str);
                }));
                if (desc.Length > 2000)
                {
                    desc = string.Join("\n", authors.Select(ns => {
                        string str = twNames[ns.Key] + ": ";
                        str       += string.Join(", ", ns.Value.Select(author =>
                        {
                            return(author);
                        }));

                        return(str);
                    }));
                }
                embed.WithDescription(desc);

                await client.SendMessageAsync(channel, deadlineInfo, embed : embed);
            }

            // Write down new first key
            LatestFetchKey[lang] = query.First()["key"].ToString();
        }
Beispiel #11
0
        public async Task Run()
        {
            // Set proper TLS settings
            ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

            // Check for a token
            string tokenPath = @"token.txt";

            if (!File.Exists(tokenPath))
            {
                Console.WriteLine("Please create a file called \"token.txt\" before running the bot!");
                Console.WriteLine("[Press any key to exit...]");
                Console.ReadKey();
                Environment.Exit(0);
            }
            Token = File.ReadAllText(tokenPath);

            // Get JSON config file
            Config.Init();

            // Initialise Discord client
            Client = new DiscordClient(new DiscordConfiguration()
            {
                AutoReconnect         = true,
                LargeThreshold        = 250,
                LogLevel              = LogLevel.Info,
                Token                 = Token,
                TokenType             = TokenType.Bot,
                UseInternalLogHandler = true,
            });

            // Initialise events
            Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", "Initialising events", DateTime.Now);

            // Get locale
            Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", string.Format("Loading {0} locale", Config.GetLang().ToUpper()), DateTime.Now);
            Locale.Init();

            // Get site information and start linking bot
            Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", "Getting wiki site information", DateTime.Now);
            Linking.Init();

            // Methods for linking bot
            Client.MessageCreated += Linking.Answer;
            Client.MessageUpdated += Linking.Edit;
            Client.MessageDeleted += Linking.Delete;

            // Start EventStreams
            if (Config.GetDomain() != "")
            {
                EventStreams.Init();
            }

            // Start Translatewiki fetches
            if (Config.GetTWChannel() != null && Config.GetTWLang() != null)
            {
                Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", $"Turning on Translatewiki ({Config.GetTWLang()})", DateTime.Now);
                TranslateWiki.Init();
            }

            // Set some events for logging the information
            Client.Ready          += Client_Ready;
            Client.GuildAvailable += Client_GuildAvailable;
            Client.ClientErrored  += Client_ClientErrored;

            // Initialise commands
            Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", "Setting up commands", DateTime.Now);
            Commands = Client.UseCommandsNext(new CommandsNextConfiguration
            {
                StringPrefix        = Config.GetValue("prefix"),
                EnableDms           = false,
                EnableMentionPrefix = true,
            });

            Commands.RegisterCommands <Configuring>();

            Commands.RegisterCommands <Streaming>();

            // Set up custom formatter
            Commands.SetHelpFormatter <LocalisedHelpFormatter>();

            // Connect and start
            Client.DebugLogger.LogMessage(LogLevel.Info, "DiscordWikiBot", "Connecting...", DateTime.Now);
            await Client.ConnectAsync();

            // Make sure not to close down automatically
            await CtrlC();
        }
        /// <summary>
        /// Form description of the changes for the project.
        /// </summary>
        /// <param name="authors">List of authors with their messages.</param>
        /// <returns>Changes in the project.</returns>
        private static string FormSimpleDescription(Dictionary <string, List <string> > authors)
        {
            var keys = authors.Keys.ToList();
            var list = keys.Select(author =>
            {
                return(string.Format("{0} ([{1}]({2}))", authors[author].Count, author, Linking.GetLink(author, "https://translatewiki.net/wiki/Special:Contribs/$1", true)));
            });
            string result = string.Join(", ", list);

            if (result.Length > MAX_EMBED_LENGTH)
            {
                result = string.Join(", ", keys.Select(author =>
                {
                    return($"{authors[author].Count} ({author})");
                }));
                if (result.Length > MAX_EMBED_LENGTH)
                {
                    string ellipsis = " […]";
                    result = result.Substring(0, MAX_EMBED_LENGTH - ellipsis.Length) + ellipsis;
                }
            }

            return(result);
        }
        /// <summary>
        /// Form description of the changes for the project.
        /// </summary>
        /// <param name="authors">List of authors with their messages.</param>
        /// <param name="includeIds">Whether to include message IDs in the result.</param>
        /// <returns>Changes in the project.</returns>
        private static string FormDescription(Dictionary <string, List <string> > authors, bool includeIds = true)
        {
            if (!includeIds)
            {
                return(FormSimpleDescription(authors));
            }
            Dictionary <string, string[]> storage = new Dictionary <string, string[]>();

            // Calculate how much links cost
            int authorLinkLength = 0;
            int authorNameLength = 0;

            foreach (var item in authors)
            {
                var arr = new string[3];
                arr[0] = string.Format("[{0}]({1})", item.Key, Linking.GetLink(item.Key, "https://translatewiki.net/wiki/Special:Contribs/$1", true));
                arr[1] = item.Value.Count.ToString();
                arr[2] = "";

                authorLinkLength += arr[0].Length;
                authorNameLength += item.Key.Length;
                storage.Add(item.Key, arr);
            }

            // Add as many message IDs as we can
            int  msgLength = 0;
            bool useLinks  = (authorLinkLength < (MAX_EMBED_LENGTH / 2));
            Dictionary <string, int>  msgNumbers = new Dictionary <string, int>();
            Dictionary <string, bool> finished   = new Dictionary <string, bool>();

            while (msgLength < MAX_EMBED_LENGTH)
            {
                foreach (var item in authors)
                {
                    if (msgLength > MAX_EMBED_LENGTH || item.Key == "1274")
                    {
                        break;
                    }
                    if (!msgNumbers.ContainsKey(item.Key))
                    {
                        msgNumbers[item.Key] = 0;
                    }

                    var num = msgNumbers[item.Key];
                    if (finished.ContainsKey(item.Key) || num >= item.Value.Count)
                    {
                        if (!finished.ContainsKey(item.Key))
                        {
                            finished[item.Key] = true;
                        }
                        continue;
                    }
                    ;

                    var arr = storage[item.Key];
                    if (num == 0)
                    {
                        arr[0]     = string.Format(" ({0})\n", (useLinks ? arr[0] : item.Key));
                        msgLength += arr[0].Length;
                        msgLength += arr[1].Length;
                    }

                    string str = (arr[2].Length > 0 ? ", " : "") + item.Value[num].Replace("_", @"\_");
                    if (msgLength + str.Length + arr[0].Length <= MAX_EMBED_LENGTH)
                    {
                        arr[2]    += str;
                        msgLength += str.Length;
                    }
                    else
                    {
                        finished[item.Key] = true;
                        continue;
                    }

                    msgNumbers[item.Key]++;
                }

                // Break if foreach has ended for all elements
                if (finished.Values.Select(x => x).ToList().Count == msgNumbers.Count)
                {
                    break;
                }
            }

            // Build the resulting string while accounting for unknown problems
            StringBuilder result = new StringBuilder();

            foreach (var item in storage)
            {
                int    trim    = (authors[item.Key].Count - msgNumbers[item.Key]);
                string trimmed = (trim > 0 ? $" + {trim}" : "");

                result.Append(item.Value[2]);
                result.Append(trimmed);
                if (useLinks)
                {
                    result.Append((result.Length + item.Value[0].Length > MAX_EMBED_LENGTH ? $" ({item.Key})\n" : item.Value[0]));
                }
                else
                {
                    result.Append(item.Value[0]);
                }
            }

            return(result.ToString().TrimEnd('\n'));
        }