User:DefaultsortBot/Source

From Wikipedia, the free encyclopedia
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Web;
using WikiFunctions;

namespace DefaultsortBot
{
    class DefaultsortBot
    {
        private System.Net.CookieCollection cookies;
        private string articleCode;
        private string talkCode;
        private DateTime lastSave;
        private uint savedCount;
        private uint skipCount;
        private string BotRunning;
        private string Status;
        private string CurArticle;
        private bool ArticleChanged;
        private string[] EditSummaries;
        private string[] cks;
        private string lastArticle;
        private string lastEditSum;

        // Shamelessly copied from Template:bots/doc
        private bool DidError(string text)
        {
            // text is assumed to be the output of WebRequest.
            if (text == null) return true;
            int code;
            string[] aText = text.Split(' ');
            if (aText.Length < 2) return true;
            code = int.Parse(aText[1]);
            if (code >= 200 && code < 300) return false;
            else return true;
        }
        private bool AllowBots(string text, string user)
        {
            return !Regex.Match(text, @"\{\{(nobots|bots\|(allow=none|deny=.*?" + user.Normalize() + @".*?|optout=all|deny=all))\}\}", RegexOptions.IgnoreCase).Success;
        }
        private void RequestEditToken(string article, out string token, out string timestamp)
        {
            // Make sure we're logged in.
            ForceLogin();
            // Get an edit token for the page, as well as a timestamp to prevent edit conflicts.
            Status = "Requesting an edit token for article";
            RedrawScreen();

            string request = "GET /w/api.php?action=query&prop=info|revisions&format=xml&intoken=edit&titles=" +
                HttpUtility.UrlEncode(article) + " HTTP/1.1\r\nAccept: text/xml\r\nAccept-Charset: utf-8\r\n" +
                "Host: en.wikipedia.org\r\nUser-agent: DefaultsortBot\r\nConnection: close\r\nCookie: ";

            string reply;
            string[] bufsplit;
            // Insert our cookies
            request = request + String.Join("; ", cks) + "\r\n\r\n";

            do
            {
                reply = WebRequest(request);
                if (DidError(reply)) Delay(10, "Error requesting edit token");
            } while (DidError(reply));

            bufsplit = reply.Split(new string[] { "\r\n\r\n" }, StringSplitOptions.None);
            token = null;
            timestamp = null;

            if (bufsplit.Length < 2) return;

            string xmlStr = bufsplit[1];
            System.Xml.XmlReader xre = System.Xml.XmlReader.Create(new System.IO.StringReader(xmlStr));

            if (!xre.ReadToFollowing("page"))
            {
                xre.Close();
                return; // malformed response
            }

            if (!xre.MoveToAttribute("edittoken"))
            {
                xre.Close();
                return;
            }

            token = xre.Value;

            if (!xre.ReadToFollowing("rev"))
            {
                xre.Close();
                return;
            }
            if (!xre.MoveToAttribute("timestamp"))
            {
                xre.Close();
                return;
            }

            timestamp = xre.Value;
            xre.Close();
        }
        private string KillCrap(string text)
        {
            int j, k;
            string tmpString = text;
            FindInnermostTag(tmpString, out j, out k, "$$##", "##$$");
            while (j != -1)
            {
                tmpString = tmpString.Substring(0, j) + tmpString.Substring(k);
                FindInnermostTag(tmpString, out j, out k, "$$##", "##$$");
            }
            FindInnermostTag(tmpString, out j, out k, "$#$#", "#$#$");
            while (j != -1)
            {
                tmpString = tmpString.Substring(0, j) + tmpString.Substring(k);
                FindInnermostTag(tmpString, out j, out k, "$#$#", "#$#$");
            }
            return tmpString;
        }
        private void ForceLogin()
        // Check to see if any of our cookies have expired, and if they have, log in again.
        {
            int i;
            bool AnyCookiesExpired = false;
            string errMsg;

            for (i = 0; i < cookies.Count; i++)
            {
                if (cookies[i].Expires < DateTime.Now) AnyCookiesExpired = true;
            }
            if (cookies.Count == 0 | AnyCookiesExpired)
            {
                Status = "Attempting login";
                RedrawScreen();

                while (!AttemptLogin(out errMsg))
                {
                    // Wait 60 seconds
                    Delay(60, "Login failed: " + errMsg);
                }
            }
        }
        private string WebRequest(string request)
        {
            TcpClient tc = new TcpClient();
            UTF8Encoding enc = new UTF8Encoding();
            System.Net.Sockets.NetworkStream ns;
            int i, j, k;
            byte[] buffer = new byte[65536];
            byte[] tbuf = new byte[256];
            try
            {
                tc.Connect("en.wikipedia.org", 80);
            }
            catch
            {
                return null;
            }

            ns = tc.GetStream();

            try
            {
                ns.Write(enc.GetBytes(request), 0, enc.GetByteCount(request));
            }
            catch
            {
                return null;
            }

            j = 0;
            do
            {
                try
                {
                    k = ns.Read(tbuf, 0, 256);
                }
                catch
                {
                    return null;
                }
                for (i = 0; i < k && j < 65535; i++)
                {
                    buffer[j++] = tbuf[i];
                }
            } while (j < 65535 & k > 0);

            tc.Close();

            return enc.GetString(buffer).Replace("\0", "");
        }
        private bool AttemptLogin(out string failreason) // returns true if login was successful
        {

            failreason = "Success"; // We'll replace this later on if there's an error

            string post = "action=login&format=xml&lgname=DefaultsortBot&lgpassword=(password removed)";
            UTF8Encoding enc = new UTF8Encoding();
            string req = "POST /w/api.php HTTP/1.0\r\nHost: en.wikipedia.org\r\nConnection: close\r\n" +
                "Content-Length: " + enc.GetByteCount(post).ToString() + "\r\nAccept: text/xml\r\n" +
                "Content-Type: application/x-www-form-urlencoded\r\nAccept-Charset: utf-8\r\n" +
                "User-agent: DefaultsortBot\r\n\r\n" + post;

            string respStr = WebRequest(req);

            if (respStr == null)
            {
                failreason = "Failed to connect to server";
                return false;
            }

            string[] sections = respStr.Split(new string[] { "\r\n\r\n" }, StringSplitOptions.None);
            string[] hdrs = sections[0].Split(new string[] { "\r\n" }, StringSplitOptions.None);

            if (sections.Length < 2)
            {
                failreason = "Malformed response from server (no XML provided)";
                return false;
            }

            System.Xml.XmlReader xr = System.Xml.XmlTextReader.Create(new System.IO.StringReader(sections[1]));
            try
            {
                if (!xr.ReadToFollowing("login"))
                {
                    failreason = "Malformed response from server (no login tag present)";
                    return false;
                }

                if (!xr.MoveToAttribute("result"))
                {
                    failreason = "Malformed response from server (no result attribute present in login tag)";
                    return false;
                }
                if (!xr.Value.Equals("Success", StringComparison.InvariantCultureIgnoreCase))
                {
                    failreason = "Server rejected login: " + xr.Value;
                    return false;
                }
            }
            catch
            {
                failreason = "Malformed XML response from server";
                return false;
            }

            System.Net.Cookie c;
            string[] vals, z;

            cookies = new System.Net.CookieCollection();

            // All right, now go through our headers.
            for (int i = 0; i < hdrs.Length; i++)
            {
                if (hdrs[i].Length > 11 && hdrs[i].Trim().StartsWith("Set-Cookie:", StringComparison.InvariantCultureIgnoreCase))
                {
                    c = new System.Net.Cookie();
                    vals = hdrs[i].Trim().Substring(11).Split(';');
                    z = vals[0].Trim().Split('=');
                    if (z.Length < 2)
                    {
                        failreason = "Malformed HTTP headers";
                        return false;
                    }
                    c.Name = z[0].Trim();
                    c.Value = z[1].Trim();
                    c.Expires = DateTime.MaxValue; // default is to assume it never expires

                    for (int j = 1; j < vals.Length; j++)
                    {
                        z = vals[j].Trim().Split('=');
                        if (z.Length >= 2)
                        {
                            if (z[0].Equals("expires", StringComparison.InvariantCultureIgnoreCase))
                            {
                                try
                                {
                                    c.Expires = DateTime.Parse(z[1]);
                                }
                                catch
                                {
                                    c.Expires = DateTime.MaxValue;
                                }
                            }
                        }
                    }

                    cookies.Add(c);
                }
            }
            // Make up cks for any functions that use it.  Do 'em a favor.

            cks = new string[] { };
            for (int i = 0; i < cookies.Count; i++)
            {
                Array.Resize(ref cks, cks.Length + 1);
                cks[i] = cookies[i].Name + "=" + cookies[i].Value;
            }

            return true;
        }
        private void RedrawScreen()
        {
            ASCIIEncoding enc = new ASCIIEncoding();
            string trimStr;
            float percent;
            if (savedCount + skipCount == 0) percent = 0;
            else percent = (((float)savedCount) / ((float)(savedCount + skipCount))) * 100;
            Console.SetCursorPosition(0, 0);
            Console.Write("┌─────────────────────────────────────────────────────────────────────────────┐\n");
            Console.Write("│ DefaultsortBot                                                              │\n");
            Console.Write("├─────────────────────────────────────────────────────────────────────────────┤\n");
            Console.Write("│ Pages saved: " + savedCount.ToString().PadRight(7) +
                "                Pages skipped: " + skipCount.ToString().PadRight(7) + "                  │\n");
            Console.Write("│ Save ratio: " + (percent.ToString("F") + "%").PadRight(7) + "                                                         │\n");
            if (BotRunning.Length > 52) trimStr = BotRunning.Substring(0, 49) + "...";
            else trimStr = BotRunning.PadRight(52);
            Console.Write("│ Bot currently running: " + trimStr + " │\n");
            if (Status.Length > 67) trimStr = Status.Substring(0, 64) + "...";
            else trimStr = Status.PadRight(67);
            Console.Write("│ Status: " + trimStr + " │\n");
            if (enc.GetString(enc.GetBytes(CurArticle)).Length > 58)
                trimStr = enc.GetString(enc.GetBytes(CurArticle)).Substring(0, 55) + "...";
            else trimStr = enc.GetString(enc.GetBytes(CurArticle)).PadRight(58);
            Console.Write("│ Current article: " + trimStr + " │\n");
            Console.Write("│                                                                             │\n");
            if (enc.GetString(enc.GetBytes(lastArticle)).Length > 55)
                trimStr = enc.GetString(enc.GetBytes(lastArticle)).Substring(0, 52) + "...";
            else trimStr = enc.GetString(enc.GetBytes(lastArticle)).PadRight(55);
            Console.Write("│ Last article saved: " + trimStr + " │\n");
            if (enc.GetString(enc.GetBytes(lastEditSum)).Length > 56)
                trimStr = enc.GetString(enc.GetBytes(lastEditSum)).Substring(0, 53) + "...";
            else trimStr = enc.GetString(enc.GetBytes(lastEditSum)).PadRight(56);
            Console.Write("│ Last edit summary: " + trimStr + " │\n");
            Console.Write("└─────────────────────────────────────────────────────────────────────────────┘\n\n");
        }
        private void FindInnermostTag(string SubString, out int start, out int end, string startToken,
            string endToken)
        // Finds the earliest, fully enclosed tag in SubString, and put the start and end
        // indices in start and end (duh).  This method works well for singling out templates that
        // are nested inside of other templates.
        {
            int x = -1, y = 0;
            end = 0;
            string workString = "";

            while (x == -1)
            {
                end = SubString.IndexOf(endToken, end);
                if (end == -1)
                {
                    start = -1;
                    end = -1;
                    return;
                }

                end += endToken.Length;

                workString = SubString.Substring(0, end);
                x = workString.IndexOf(startToken);
            }

            while (x != -1)
            {
                y = x;
                x = workString.IndexOf(startToken, x + startToken.Length);
            }

            start = y;

        }
        private string StripPunctuationAndUnicode(string Input)
        {
            // Strip out any punctuation other than what's allowed, and try to replace non-"A-Z" characters with
            // their alphabetic equivelants.

            // To those of you who are reading my code, yes, this method sucks.  I hate Unicode.  I realize why
            // we need to have it, but it still sucks.  If you've got a better way of converting foreign
            // characters into their ASCII equivelants, please, I'd love to hear about it.

            string[] foreigns = {
                                    // First group is one that does not need any translation.
                                    ".,-01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ",
                                    // Each group afterwards will be replaced with an equivelant character in equivs
                                    // These groups are in no particular order, other than maybe likelihood of them occurring.
                                    "ÀÁÂÃÄÅĀĂĄǍǺΆΑẠẢẤẦẨẪẬẮẰẲẴẶ", "àáâãäåāăąǎǻαаạảấầẩẫậắằẳẵặ",
                                    "ÇĆĈĊČ", "çćĉċčс",
                                    "ÈÉÊËĒĔĖĘĚƏΈΕЁЕẸẺẼẾỀỂỄỆ", "èéêëēĕėęěəеẹẻẽếềểễệ",
                                    "ÌÍÎÏĨĪĬĮİǏΙΊІЇỈỊ", "ìíîïĩīĭįıǐΐỉịſ",
                                    "ÑŃŅŇΝ", "ñńņňʼnŋṇ",
                                    "ÒÓÔÕÖØŌŎŐƠǑǾΌΟỌỎỐỒỔỖỘỚỜỞỠỢ", "ðòóôõöøōŏőơǒǿоọỏốồổỗộớờởỡợ",
                                    "ÙÚÛÜŨŪŬŮŰŲƯǓǕǗǙǛỤỦỨỪỬỮỰ", "ùúûüũūŭůűųưǔǖǘǚǜụủứừửữự",
                                    "ÝŶŸΎỲỴỶỸΥ", "ýÿŷỳỵỷỹу",
                                    "ÆǼ", "æǽ",
                                    "ßΒβВ", "в",
                                    "ÐĎĐ", "ďđḍ",
                                    "ĜĞĠĢ", "ĝğġģ",
                                    "ĤĦΉΗ", "ĥħн",
                                    "IJ", "ij",
                                    "ĴЈ", "ĵ",
                                    "ĶĸΚ", "ķк",
                                    "ĹĻĽĿŁ", "ĺļľŀł",
                                    "Œ", "œ",
                                    "ŔŖŘ", "ŕŗř",
                                    "ŚŜŞŠЅṢ", "śŝşšṣ",
                                    "ŢŤŦΤ", "ţťŧт",
                                    "ŴẀẂẄ", "ŵẁẃẅ",
                                    "ŹŻŽΖ", "źżž",
                                    "ƒ",
                                    "Μ", "м",
                                    "Ρ", "р",
                                    "Χ",
                                    "­­­", "/"
                                };

            string[] equivs = {
                                  "", "A", "a", "C", "c", "E", "e", "I", "i", "N", "n", "O", "o", "U", "u", "Y",
                                  "y", "Ae", "ae", "B", "b", "D", "d", "G", "g", "H", "h", "Ij", "ij", "J", "j",
                                  "K", "k", "L", "l", "Ce", "ce", "R", "r", "S", "s", "T", "t", "W", "w", "Z",
                                  "z", "f", "M", "m", "P", "p", "X", "-", " " };

            int x, y, z;
            char[] curChars;
            char[] workString = Input.ToCharArray();
            string output = "";
            bool found = false;

            for (x = 0; x < workString.Count(); x++)
            {
                found = false;
                // If this is one of our tokens, skip over it.
                if (Input.Length >= x + 4)
                {
                    if (
                        Input.Substring(x, 4).Equals("$$##") ||
                        Input.Substring(x, 4).Equals("$#$#") ||
                        Input.Substring(x, 4).Equals("#$#$") ||
                        Input.Substring(x, 4).Equals("##$$"))
                    {
                        x += 3;
                        continue;
                    }
                }
                for (y = 0; y < foreigns.Count() & !found; y++)
                {
                    curChars = foreigns[y].ToCharArray();
                    for (z = 0; z < curChars.Count() & !found; z++)
                    {
                        if (workString[x].Equals(curChars[z]))
                        {
                            if (y == 0) output = output + workString[x].ToString();
                            else output = output + equivs[y];
                            found = true;
                        }
                    }
                }
            }

            return output;
        }
        private string Detag(string inString, out string[] tags, out string[] cTags)
        {
            string newText = inString;
            int start, end;
            tags = new string[0];
            cTags = new string[0];
            // Arrayerize all of the template tags, comments, and anything inside of <nowiki> tags in the
            // article, and replace them with tokens of the format "$$##x##$$" (or "$#$#x#$#$" for
            // comments/nowiki tags), where x = tagCount.  This makes it easier to remove them from the
            // article, modify them, and put them back in at the end.

            // First off, take out anything in comments.
            int tagCount = 0;
            FindInnermostTag(newText, out start, out end, "<!--", "-->");
            while (start != -1)
            {
                Array.Resize(ref cTags, cTags.Length + 1);
                cTags[cTags.Length - 1] = newText.Substring(start, end - start);
                newText = newText.Substring(0, start) + "$#$#" + tagCount++.ToString() + "#$#$" + newText.Substring(end);
                FindInnermostTag(newText, out start, out end, "<!--", "-->");
            }

            // Now anything inside of <nowiki> tags.
            FindInnermostTag(newText, out start, out end, "<nowiki>", "</nowiki>");

            while (start != -1)
            {
                Array.Resize(ref cTags, cTags.Length + 1);
                cTags[cTags.Length - 1] = newText.Substring(start, end - start);
                newText = newText.Substring(0, start) + "$#$#" + tagCount++.ToString() + "#$#$" + newText.Substring(end);
                FindInnermostTag(newText, out start, out end, "<nowiki>", "</nowiki>");
            }

            tagCount = 0;

            FindInnermostTag(newText, out start, out end, "{{", "}}");
            while (start != -1)
            {
                Array.Resize(ref tags, tags.Length + 1);
                // I don't care about the opening and closing braces, we'll put those back in when we put the
                // article back together.
                tags[tags.Length - 1] = newText.Substring(start + 2, end - start - 4);
                newText = newText.Substring(0, start) + "$$##" + tagCount++.ToString() + "##$$" + newText.Substring(end);
                FindInnermostTag(newText, out start, out end, "{{", "}}");
            }

            return newText;
        }
        private void Delay(double secs, string reason)
        {
            DateTime retryTimer;
            int j, k;

            retryTimer = DateTime.Now.AddSeconds(secs);
            j = 0;
            while (retryTimer > DateTime.Now)
            {
                k = (int)(retryTimer.Subtract(DateTime.Now).TotalSeconds * 10);
                if (k != j)
                {
                    j = k;
                    Status = reason + ", retrying in " + (((double)k) / 10).ToString("F1") + " seconds";
                    RedrawScreen();
                }
            }
        }
        private void AlertNewMessages()
        {
            System.Console.Write("You have new messages!  Read them, then hit enter to continue.  ");
            System.Console.ReadLine();

            // Attempt to clear out the notification
            string request, reply;

            request = "GET /wiki/User_talk:DefaultsortBot" +
                " HTTP/1.1\r\nAccept: text/xml\r\nAccept-Charset: utf-8\r\n" +
                "Host: en.wikipedia.org\r\nUser-agent: DefaultsortBot\r\nConnection: close\r\nCookie: " +
                String.Join("; ", cks) + "\r\n\r\n";

            Console.Clear();

            do
            {
                Status = "Clearing out new message alert";
                RedrawScreen();
                reply = WebRequest(request);
                if (reply == null) Delay(10, "Error clearing out new message alert");
            } while (reply == null);
        }

        private bool IsWPBioTag(string workString)
        // There's about 13 templates that redirect to WPBiography.  Check for 'em all.
        {
            if (
                workString.Equals("Musician", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("Bio", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("WikiProject Biography", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("WPBiography", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("BRoy", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("WikiProjectBiography", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("WPBIO", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("WP Biography", StringComparison.InvariantCultureIgnoreCase) ||
                workString.Equals("WP Bio", StringComparison.InvariantCultureIgnoreCase)) return true;
            return false;
        }

        private void DefaultsortBot1()
        {
            string[] tagsA = new string[] { }, tagsB, tagsC = new string[] { };
            string[] cTagsA = new string[] { };
            int tagCount = 0;
            string newText;
            string chReason = "added DEFAULTSORT to page (used a WikiProject banner's listas parameter on the talk page)";
            bool foundOther = false;
            bool foundWPB = false;
            string foString = "";
            int j, k;
            string inString = talkCode;

            BotRunning = "DefaultsortBot 1";
            Status = "DefaultsortBot 1 processing";
            RedrawScreen();
            string[] aTags, acTags;

            string articleText = Detag(articleCode, out aTags, out acTags);
            // First off, if the article already has a DEFAULTSORT, no point in going any further.
            foreach (string tag in aTags)
            {
                if (KillCrap(tag).Trim().StartsWith("DEFAULTSORT", StringComparison.InvariantCultureIgnoreCase))
                    return;
            }

            // A lot of the code that follows was copied from ListasBot 1 and 4.

            newText = Detag(inString, out tagsA, out cTagsA);

            // Now, figure out which ones have our listas and DEFAULTSORT tags.
            foreach (string s in tagsA)
            {
                tagsB = s.Split('|');

                if (IsWPBioTag(tagsB[0].Trim()))
                {
                    foundWPB = true;
                }
                if (tagsB[0].StartsWith("DEFAULTSORT", StringComparison.InvariantCultureIgnoreCase))
                {
                    // Because DEFAULTSORT could be separated by either a pipe (preferred) or a colon (not preferred).
                    tagsC = s.Split(new char[] { '|', ':' });
                    if (tagsC.Count() == 2 & !foundOther)
                    {
                        // If we have a comment token in here, take it out for the purpose of identifying
                        // our tag.
                        foString = tagsC[1].Trim();
                        FindInnermostTag(foString, out j, out k, "$#$#", "#$#$");
                        while (j != -1)
                        {
                            foString = foString.Substring(0, j) + foString.Substring(k);
                            FindInnermostTag(foString, out j, out k, "$#$#", "#$#$");
                        }

                        if (foString.Trim().Length > 0)
                        {
                            foundOther = true;
                            foString = foString.Trim();
                        }
                    }
                }
                foreach (string t in tagsB)
                {
                    if (t.Length >= 6 && t.Substring(0, 6).Equals("listas", StringComparison.InvariantCultureIgnoreCase))
                    {
                        tagsC = t.Split('=');

                        // If we have a comment token in here, take it out for the purpose of identifying
                        // our tag.
                        if (tagsC.Count() == 2 & !foundOther)
                        {
                            foString = tagsC[1].Trim();
                            FindInnermostTag(foString, out j, out k, "$#$#", "#$#$");
                            while (j != -1)
                            {
                                foString = foString.Substring(0, j) + foString.Substring(k);
                                FindInnermostTag(foString, out j, out k, "$#$#", "#$#$");
                            }

                            if (foString.Trim().Length > 0)
                            {
                                foundOther = true;
                            }
                        }
                    }
                }
            }

            if (!foundWPB) return; // no WPBiography template, no point in going any further

            // If we didn't find another listas or DEFAULTSORT tag, check the title.  If it's a single word
            // (e.g., no spaces), use that as our listas parameter.
            string tmpString;
            if (!foundOther)
            {
                tagsB = CurArticle.Trim().Split(':');
                if (tagsB.Count() == 2) tmpString = tagsB[1].Trim();
                else tmpString = CurArticle.Trim();

                tagsC = tmpString.Split(' ');
                // Make sure that there's only one word, and if we're not in article space, make sure we're not
                // in File talk space, User space, or User Talk space
                if (tagsC.Count() == 1 &
                    (((tagsB.Count() == 2) & (Namespace.Determine(CurArticle) != (int)Namespaces.ImageTalk) &
                    (Namespace.Determine(CurArticle) != (int)Namespaces.User) &
                    (Namespace.Determine(CurArticle) != (int)Namespaces.UserTalk)) |
                    (tagsB.Count() == 1)))
                {
                    chReason = "added DEFAULTSORT to article (used article title since it was a single word)";
                    foString = tmpString;

                }
                // Is this a category talk page?  If so, we can use the straight name of the page.
                else if (tagsB.Count() == 2 & Namespace.Determine(CurArticle) == (int)Namespaces.CategoryTalk)
                {
                    chReason = "added DEFAULTSORT to article (used category talk page title)";
                    foString = tmpString;
                }
                else
                {
                    return;
                }
            }

            // Strip illegal characters.
            foString = StripPunctuationAndUnicode(foString).Trim();

            // Reformat the string -- specifically, make sure that any commas in the string have spaces
            // immediately after them.
            tagsB = foString.Split(',');
            for (tagCount = 0; tagCount < tagsB.Length; tagCount++)
            {
                tagsB[tagCount] = tagsB[tagCount].Trim();
            }

            foString = String.Join(", ", tagsB);

            // Append a DEFAULTSORT to the end of the article.
            articleCode = articleCode.Trim() + "\r\n\r\n{{DEFAULTSORT:" + foString + "}}\r\n";

            Array.Resize(ref EditSummaries, EditSummaries.Length + 1);
            EditSummaries[EditSummaries.Length - 1] = chReason;

            ArticleChanged = true;
        }

        public void Main()
        {
            string WorkingCat;
            savedCount = 0;
            skipCount = 0;
            string ContinueVal;
            string errMsg;
            string url;
            string xmlStr;
            System.Xml.XmlReader xr, xrc, xrcm;
            string editToken;
            string timeStamp;
            string request, reply, postStr;
            string[] bufsplit;
            string editSummary;
            UTF8Encoding enc = new UTF8Encoding();
            Article a;

            Status = "";
            CurArticle = "";
            BotRunning = "";
            cookies = new System.Net.CookieCollection();

            WorkingCat = "Category:Biography articles with listas parameter";
            a = new Article(WorkingCat);

            Console.Write("Starting value (or enter for none)? ");
            ContinueVal = Console.ReadLine().Trim();

            if (ContinueVal.Length == 0) ContinueVal = null;
            else ContinueVal = ContinueVal + "|";

            lastArticle = "";
            lastEditSum = "";

            Console.Clear();

            // We need one successful login attempt to start
            if (!AttemptLogin(out errMsg))
            {
                Status = "Login failed: " + errMsg;
                RedrawScreen();
                return;
            }
            else
            {
                Status = "Login succeeded";
                RedrawScreen();
            }

            lastSave = DateTime.Now;

            // Main loop
            do
            {

                ForceLogin();

                Status = "Fetching 500 articles from server";
                RedrawScreen();

                // Get a list of pages.
                url = "http://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=" +
                    a.URLEncodedName + "&cmlimit=500&format=xml";
                if (ContinueVal != null) url = url + "&cmcontinue=" + ContinueVal;

                // xr won't be null once the do loop is over, but C# considers it to be an error
                // to use a variable that hasn't been assigned a value, and since xr is assigned
                // a value inside the do loop...
                xr = null;

                do
                {
                    try
                    {
                        xmlStr = Tools.GetHTML(url);
                    }
                    catch
                    {
                        xmlStr = null;
                        // Wait 10 seconds
                        Delay(10, "Failed to fetch list of articles");
                    }

                    try
                    {
                        xr = System.Xml.XmlReader.Create(new System.IO.StringReader(xmlStr));
                    }
                    catch
                    {
                        xmlStr = null;
                        Delay(10, "Failed to create XML reader");
                    }

                    if (!xr.ReadToFollowing("query"))
                    {
                        xmlStr = null;
                        Delay(10, "Invalid XML response from server");
                    }
                } while (xmlStr == null);


                if (xr.ReadToFollowing("query-continue"))
                {
                    xrc = xr.ReadSubtree();
                    if (xrc.ReadToFollowing("categorymembers"))
                    {
                        if (xrc.MoveToAttribute("cmcontinue"))
                        {
                            ContinueVal = xrc.Value;
                        }
                        else ContinueVal = null;
                    }
                    else ContinueVal = null;

                    xrc.Close();
                }
                else ContinueVal = null;

                xr.Close();
                xr = System.Xml.XmlReader.Create(new System.IO.StringReader(xmlStr));

                while (xr.ReadToFollowing("cm"))
                {
                    Status = "Checking for new messages";
                    RedrawScreen();
                    // Check for new messages.  Gotta be logged in for this one.
                    request = "GET /w/api.php?action=query&meta=userinfo&uiprop=hasmsg&format=xml" +
                        " HTTP/1.1\r\nAccept: text/xml\r\nAccept-Charset: utf-8\r\n" +
                        "Host: en.wikipedia.org\r\nUser-agent: DefaultsortBot\r\nConnection: close\r\nCookie: " +
                        String.Join("; ", cks) + "\r\n\r\n";

                    reply = WebRequest(request);
                    if (!DidError(reply))
                    {
                        bufsplit = reply.Split(new string[] { "\r\n\r\n" }, StringSplitOptions.None);
                        if (bufsplit.Length >= 2)
                        {
                            try
                            {
                                xrcm = System.Xml.XmlReader.Create(new System.IO.StringReader(bufsplit[1]));
                            }
                            catch
                            {
                                xrcm = null;
                            }
                            if (xrcm != null)
                            {
                                try
                                {
                                    if (xrcm.ReadToFollowing("userinfo"))
                                    {
                                        if (xrcm.MoveToAttribute("messages"))
                                        {
                                            AlertNewMessages();
                                        }
                                    }
                                }
                                catch { }
                                xrcm.Close();
                            }
                        }
                    }

                    if (!xr.MoveToAttribute("title")) continue; // No title attribute, move on to the next one
                    CurArticle = xr.Value;
                    Status = "Fetching talk page code";
                    RedrawScreen();

                    do
                    {
                        try
                        {
                            talkCode = Tools.GetArticleText(CurArticle);
                        }
                        catch
                        {
                            talkCode = null;
                            Delay(10, "Error fetching talk page code");
                        }
                    } while (talkCode == null);
                    ArticleChanged = false;
                    EditSummaries = new string[0];

                    // Get an edit token for the page, as well as a timestamp to prevent edit conflicts.
                    // For this bot, we want the article page, not the talk page.
                    do
                    {
                        RequestEditToken(Tools.ConvertFromTalk(new Article(CurArticle)), out editToken,
                            out timeStamp);
                    } while (editToken == null);

                    if (!AllowBots(talkCode, "DefaultsortBot"))
                    {
                        skipCount++;
                        continue; // Can't do anyting if bots aren't allowed
                    }

                    Status = "Fetching article code";
                    RedrawScreen();
                    try
                    {
                        articleCode = Tools.GetArticleText(Tools.ConvertFromTalk(new Article(CurArticle)));
                    }
                    catch
                    {
                        articleCode = null;
                    }

                    if(articleCode != null) DefaultsortBot1();
                    BotRunning = "";
                    RedrawScreen();

                    if (ArticleChanged)
                    {
                        editSummary = String.Join(", ", EditSummaries);
                        editSummary = char.ToUpper(editSummary[0]).ToString() + editSummary.Substring(1) +
                            ". [[User talk:DefaultsortBot|Did I get it wrong?]]";
                        postStr = "action=edit&format=xml&title=" +
                            HttpUtility.UrlEncode(Tools.ConvertFromTalk(new Article(CurArticle))) +
                            "&text=" + HttpUtility.UrlEncode(articleCode) +
                            "&token=" + HttpUtility.UrlEncode(editToken) +
                            "&summary=" + HttpUtility.UrlEncode(editSummary) +
                            "&basetimestamp=" + HttpUtility.UrlEncode(timeStamp) +
                            "&notminor&bot=";

                        request = "POST /w/api.php HTTP/1.1\r\nAccept: text/xml\r\nAccept-Charset: utf-8\r\n" +
                            "Host: en.wikipedia.org\r\nUser-agent: DefaultsortBot\r\nConnection: close\r\n" +
                            "Content-Type: application/x-www-form-urlencoded\r\nContent-Length: " +
                            enc.GetByteCount(postStr).ToString() + "\r\nCookie: " +
                            String.Join("; ", cks) + "\r\n\r\n" + enc.GetString(enc.GetBytes(postStr));

                        // Delay 10 seconds from last save.
                        // We can disable this for testing purposes
/*
                        if (lastSave.AddSeconds(10) > DateTime.Now)
                        {
                            Delay(lastSave.AddSeconds(10).Subtract(DateTime.Now).TotalSeconds, "Waiting to save");
                        }

                        Status = "Saving page";
                        RedrawScreen();

                        do
                        {
                            reply = WebRequest(request);
                            if (reply == null) Delay(10, "Error saving page");
                        } while (reply == null);
                        lastSave = DateTime.Now;
 */
                        savedCount++;

                        lastEditSum = editSummary;
                        lastArticle = CurArticle;
                    }
                    else
                    {
                        skipCount++;
                    }
                }
                xr.Close();

            } while (ContinueVal != null);

        }

    }
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            DefaultsortBot b = new DefaultsortBot();
            b.Main();
        }
    }
}