Commons:Photo challenge/code/voting.cs
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; }
}
}
}