Commons:Photo challenge/code/voting.cs

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
//Voting code
//Author user:colin
//License: Public domain
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using HtmlAgilityPack;

namespace Voting
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            const string aChallenge = "2023 - November - Too many to count";

            string challenge = args.Length > 0 ? args[0] : aChallenge;

            string outFile = (challenge + ".txt").Replace('/', '-');

            bool hasFilePairs = false;

            // Will need to work on this for multi-month challenges
            string theme = challenge;
            string month = "";
            string year = "";
            string[] parts = challenge.Split(new[] { " - " }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 3)
            {
                year = parts[0];
                month = parts[1];
                theme = parts[2];
            }

            string url = "https://commons.wikimedia.org/w/index.php?title=Commons:Photo_challenge/" +
                         challenge.Replace(' ', '_') + "/Voting&action=raw";

            using (var errorWriter = new StreamWriter("Errors-" + outFile, false, Encoding.UTF8))
            {
                var infos = new List<FileInfo>();
                var voters = new Dictionary<string, Voter>();

                using (var revisedWriter = new StreamWriter("Revised-" + outFile, false, Encoding.UTF8))
                {
                    List<string> wikiText = await DownloadWikiFile(url, errorWriter);

                    if (wikiText == null)
                    {
                        errorWriter.WriteLine("No wikitext");
                        return;
                    }

                    revisedWriter.WriteLine("{{Discussion top}}");
                    FileInfo info = null;

                    foreach (string line in wikiText)
                    {
                        string revisedLine = Revise(line);
                        string lowerLine = line.ToLower();
                        if (revisedLine != null)
                        {
                            revisedWriter.WriteLine(revisedLine);
                        }

                        if (line.StartsWith("==="))
                        {
                            info = new FileInfo();
                            infos.Add(info);
                            continue;
                        }

                        int startFile = line.IndexOf("[[File:");
                        if (startFile != -1)
                        {
                            string fileLine = line.Substring(startFile);
                            string[] segments = fileLine.Split(new[] { '|' });
                            if (segments.Length < 5)
                            {
                                errorWriter.WriteLine("Bad file: " + fileLine);
                                continue;
                            }
                            if (segments.Length > 5)
                            {
                                for (int i = 5; i < segments.Length; ++i)
                                {
                                    segments[4] = segments[4] + '|' + segments[i];
                                }
                            }

                            if (string.IsNullOrEmpty(info.FileName))
                            {
                                info.FileName = segments[0].Substring(7); // [[File:
                            }
                            else
                            {
                                info.FileName2 = segments[0].Substring(7); // [[File:
                                hasFilePairs = true;
                            }

                            Console.WriteLine();
                            Console.Write(info.FileName);
                            int openBracket = segments[4].IndexOf('[');
                            if (openBracket == -1)
                            {
                                openBracket = segments[4].IndexOf(']');
                            }

                            if (openBracket == -1)
                            {
                                errorWriter.WriteLine("Bad file bracket: " + fileLine);
                                continue;
                            }

                            info.Title = segments[4].Substring(0, openBracket).Trim();
                            continue;
                        }

                        if (line.StartsWith("<!-- '''C") || line.StartsWith("'''C"))
                        {
                            int bar = line.IndexOf('|');
                            if (bar == -1)
                            {
                                errorWriter.WriteLine("Bad file |: " + line);
                                continue;
                            }

                            int closeBracket = line.IndexOf(']', bar);
                            if (closeBracket == -1)
                            {
                                errorWriter.WriteLine("Bad file ]: " + line);
                                continue;
                            }

                            info.Creator = line.Substring(bar + 1, closeBracket - bar - 1);
                            continue;
                        }

                        if (line.Contains("*}}") && info.Creator != null && !line.Contains("('''Vote invalid''')"))
                        {
                            var vote = new Vote();

                            int userOffset = 7;
                            int user = -1;
                            foreach (string userPrefix in new List<string> { "[[user:", "[[benutzer:", "[[:es:usuario:" })
                            {
                                user = lowerLine.IndexOf(userPrefix);
                                userOffset = userPrefix.Length;
                                if (user != -1)
                                {
                                    break;
                                }
                            }

                            if (user == -1)
                            {
                                errorWriter.WriteLine("Bad user [User: " + line);
                                continue;
                            }

                            int bar = line.IndexOf('|', user);
                            int endUser = line.IndexOf("]]", user);

                            if (bar == -1 && endUser == -1)
                            {
                                errorWriter.WriteLine("Bad user user| " + line);
                                continue;
                            }

                            int start = user + userOffset;
                            int end = bar == -1 ? endUser : bar;
                            string name = line.Substring(start, end - start);

                            Voter voter;
                            if (!voters.TryGetValue(name, out voter))
                            {
                                voter = new Voter {Name = name, NbrContributions = await GetContributions(name, errorWriter)};

                                voters.Add(name, voter);
                            }

                            vote.Voter = voter;

                            if (line.Contains("{{3/3*}}"))
                            {
                                vote.Award = 3;
                            }
                            else if (line.Contains("{{2/3*}}"))
                            {
                                vote.Award = 2;
                            }
                            else if (line.Contains("{{1/3*}}"))
                            {
                                vote.Award = 1;
                            }
                            else
                            {
                                vote.Award = 0;
                            }

                            if (name == info.Creator)
                            {
                                errorWriter.WriteLine("User:" + name + " voted on own image: " + info.FileName);
                            }

                            if (vote.Award > 0)
                            {
                                voter.Votes.Add(vote.Award);
                            }

                            info.Votes.Add(vote);
                        }
                    }

                    revisedWriter.WriteLine("{{Discussion bottom}}");
                }

                foreach (Voter voter in voters.Values.OrderBy(v => v.Name))
                {
                    ////  errorWriter.WriteLine("Voter: " + voter.Name);
                    if (voter.NbrContributions < 50 && infos.All(c => c.Creator != voter.Name))
                    {
                        errorWriter.WriteLine("https://commons.wikimedia.org/wiki/Special:Contributions/" + voter.Name.Replace(' ' , '_') +
                                              " has too few contributions");
                    }

                    if (voter.Votes.Count > 3 || voter.Votes.Count(v => v == 1) > 1 ||
                        voter.Votes.Count(v => v == 2) > 1 || voter.Votes.Count(v => v == 3) > 1)
                    {
                        string voteList = string.Join(", ", voter.Votes.OrderByDescending(v => v));

                        errorWriter.WriteLine("https://commons.wikimedia.org/wiki/Special:Contributions/" + voter.Name.Replace(' ', '_') +
                                              " voted wrongly: " + voteList);
                    }
                }

                int nbrContributors = infos.Select(i => i.Creator).Distinct().Count();
                int nbrVoters = voters.Count();
                int nbrImages = infos.Count();

                using (var writer = new StreamWriter(outFile, false, Encoding.UTF8))
                {
                    List<FileInfo> ranks = infos.OrderByDescending(i => i.Score).ThenByDescending(i => i.Votes.Count).ToList();
                    FileInfo r1 = ranks[0];
                    FileInfo r2 = ranks[1];
                    FileInfo r3 = ranks[2];

                    int rank1 = 1;
                    int rank2 = 2;
                    int rank3 = 3;
                    bool nonStandardRank = false;

                    if (r1.Score == r2.Score && r1.Votes.Count == r2.Votes.Count)
                    {
                        rank2 = 1;
                        nonStandardRank = true;
                    }

                    if (r2.Score == r3.Score && r2.Votes.Count == r3.Votes.Count)
                    {
                        rank3 = (rank1 == rank2) ? 1 : 2;
                        nonStandardRank = true;
                    }

                    writer.WriteLine("{{Photo challenge winners table");
                    writer.WriteLine("|page=Photo challenge/" + challenge);
                    writer.WriteLine("|theme    = " + theme);
                    writer.WriteLine("|image_1  = " + r1.FileName);
                    writer.WriteLine("|image_2  = " + r2.FileName);
                    writer.WriteLine("|image_3  = " + r3.FileName);
                    writer.WriteLine("|title_1  = " + r1.Title);
                    writer.WriteLine("|title_2  = " + r2.Title);
                    writer.WriteLine("|title_3  = " + r3.Title);
                    writer.WriteLine("|author_1 = " + r1.Creator);
                    writer.WriteLine("|author_2 = " + r2.Creator);
                    writer.WriteLine("|author_3 = " + r3.Creator);
                    writer.WriteLine("|score_1  = " + r1.Score);
                    writer.WriteLine("|score_2  = " + r2.Score);
                    writer.WriteLine("|score_3  = " + r3.Score);
                    if (nonStandardRank)
                    {
                        writer.WriteLine("|rank_1  = " + rank1);
                        writer.WriteLine("|rank_2  = " + rank2);
                        writer.WriteLine("|rank_3  = " + rank3);
                    }
                    writer.WriteLine("}}");

                    writer.WriteLine();
                    writer.WriteLine("https://commons.wikimedia.org/wiki/User_talk:{0}", r1.Creator.Replace(' ', '_'));
                    writer.WriteLine("[[Commons:Photo challenge/" + challenge + "/Winners]]");
                    writer.WriteLine(
                        "{{{{Photo Challenge {0}|File:{1}|{2}|{3}|{4}}}}}",
                        rank1 == 1 ? "Gold" : rank1 == 2 ? "Silver" : "Bronze",
                        r1.FileName,
                        theme,
                        year,
                        month);
                    writer.WriteLine();
                    writer.WriteLine("https://commons.wikimedia.org/wiki/User_talk:{0}", r2.Creator.Replace(' ', '_'));
                    writer.WriteLine("[[Commons:Photo challenge/" + challenge + "/Winners]]");
                    writer.WriteLine(
                        "{{{{Photo Challenge {0}|File:{1}|{2}|{3}|{4}}}}}",
                        rank2 == 1 ? "Gold" : rank2 == 2 ? "Silver" : "Bronze",
                        r2.FileName,
                        theme,
                        year,
                        month);
                    writer.WriteLine();
                    writer.WriteLine("https://commons.wikimedia.org/wiki/User_talk:{0}", r3.Creator.Replace(' ', '_'));
                    writer.WriteLine("[[Commons:Photo challenge/" + challenge + "/Winners]]");
                    writer.WriteLine(
                        "{{{{Photo Challenge {0}|File:{1}|{2}|{3}|{4}}}}}",
                        rank3 == 1 ? "Gold" : rank3 == 2 ? "Silver" : "Bronze",
                        r3.FileName,
                        theme,
                        year,
                        month);
                    writer.WriteLine();

                    writer.WriteLine("{{Commons:Photo challenge/" + challenge + "/Winners}}");
                    writer.WriteLine("Congratulations to [[User:{0}|]], [[User:{1}|]] and [[User:{2}|]]. -- ~~~~", r1.Creator, r2.Creator, r3.Creator);

                    writer.WriteLine();
                    writer.WriteLine("*Nbr contributors: " + nbrContributors);
                    writer.WriteLine("*Nbr voters: " + nbrVoters);
                    writer.WriteLine("*Nbr images: " + nbrImages);
                    writer.WriteLine();
                    writer.WriteLine("The Score is the sum of the 3*/2*/1* votes. The Support is the count of 3*/2*/1* votes and 0* likes. In the event of a tie vote, the support decides the rank.");
                    writer.WriteLine();

                    writer.WriteLine("{| class=\"sortable wikitable\"");
                    writer.WriteLine("|-");
                    writer.WriteLine(
                        "! Image" + (hasFilePairs ? "1 !! Image2" : string.Empty) + " !! Author !! data-sort-type=\"number\" | Rank !! data-sort-type=\"number\" | Score !! data-sort-type=\"number\" | Support");

                    int rank = 0;
                    int previousScore = -1;
                    int previousSupport = -1;
                    foreach (FileInfo info in ranks)
                    {
                        if (info.Votes.Count == 0)
                        {
                            break;
                        }

                        if (info.Score != previousScore || info.Votes.Count != previousSupport)
                        {
                            previousScore = info.Score;
                            previousSupport = info.Votes.Count;
                            ++rank;
                        }

                        writer.WriteLine("|-");
                        writer.WriteLine(
                            "| [[File:{0}|120px]]" + (hasFilePairs ? " || [[File:{5}|120px]]" : string.Empty) + " || [[User:{1}|{1}]] || {2} || {3} || {4}",
                            info.FileName,
                            info.Creator,
                            rank,
                            info.Score,
                            info.Votes.Count,
                            info.FileName2);
                    }

                    writer.WriteLine("|}");
                }
            }
        }

        private static async Task<List<string>> DownloadWikiFile(string url, StreamWriter errorWriter)
        {
            HttpClient client = new HttpClient();
            HttpResponseMessage response = null;
            try
            {
                response = await client.GetAsync(url);
                response.EnsureSuccessStatusCode();
            }
            catch (Exception e)
            {
                errorWriter.WriteLine(url + " gave " + e.Message);
                return null;
            }

            if ((response.StatusCode == HttpStatusCode.OK ||
                 response.StatusCode == HttpStatusCode.Moved ||
                 response.StatusCode == HttpStatusCode.Redirect) &&
                response.Content.Headers.ContentType.MediaType.StartsWith("text", StringComparison.OrdinalIgnoreCase))
            {
                var result = new List<string>();
                // if the remote file was found, download it
                using (Stream inputStream = await response.Content.ReadAsStreamAsync())
                {
                    using (TextReader reader = new StreamReader(inputStream))
                    {
                        while (true)
                        {
                            string line = reader.ReadLine();
                            if (line == null)
                            {
                                return result;
                            }

                            result.Add(line);
                        }
                    }
                }
            }

            errorWriter.WriteLine(url + " gave " + response.StatusCode);
            return null;
        }

        private static string Revise(string line)
        {
            if (line.StartsWith("<!-- '''Creator"))
            {
                line = line.Replace("<!-- ", string.Empty);
                line = line.Replace(" -->", string.Empty);
                line = line.Replace("{{Collapse top|Current votes – please choose your own winners before looking}}",
                    string.Empty);
            }
            else if (line.StartsWith("{{Collapse bottom}}"))
            {
                line = null;
            }
            else if (line.StartsWith("'''Voting will end"))
            {
                line = line.Replace("Voting will end", "Voting ended");
            }

            return line;
        }

        private static async Task<int> GetContributions(string user, StreamWriter errorWriter)
        {
            Console.Write(".");
            string url = "https://commons.wikimedia.org/wiki/Special:Contributions/" + user;

            HttpClient client = new HttpClient();
            HttpResponseMessage response = null;
            try
            {
                response = await client.GetAsync(url);
                response.EnsureSuccessStatusCode();
            }
            catch (Exception e)
            {
                errorWriter.WriteLine(url + " gave " + e.Message);
                return 0;
            }

            using (Stream receiveStream = await response.Content.ReadAsStreamAsync())
            {
                var doc = new HtmlDocument();
                doc.Load(receiveStream, Encoding.UTF8);

                HtmlNode docNode = doc.DocumentNode;

                HtmlNodeCollection nextLink = docNode.SelectNodes("//a[contains(@class, 'mw-nextlink')]");

                return nextLink == null ? 0 : 50;
            }
        }

        public class
            FileInfo
        {
            public FileInfo()
            {
                Votes = new List<Vote>();
            }

            public string FileName { get; set; }

            public string FileName2 { get; set; }
            public string Title { get; set; }
            public string Creator { get; set; }

            public int Score
            {
                get { return Votes.Sum(v => v.Award); }
            }

            public List<Vote> Votes { get; set; }
        }

        public class Vote
        {
            public Voter Voter { get; set; }
            public int Award { get; set; }
        }

        public class Voter
        {
            public Voter()
            {
                Votes = new List<int>();
            }

            public string Name { get; set; }
            public int NbrContributions { get; set; }
            public List<int> Votes { get; set; } 
        }
    }
}