User:MER-C/FPC.java
Appearance
/**
* @(#)FPC.java 0.01 24/08/2008
* Copyright (C) 2008 MER-C
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.security.auth.login.*;
import javax.swing.*;
import javax.swing.table.*;
import wizard.*;
/**
* FPC closing program. Requires Wiki.java 0.17,
* @author MER-C
* @version 0.01
*/
public class FPC
{
/*
TODO:
* Replace with featured version
* Wikipedia:Featured pictures and subgallery
*/
// English Wikipedia reference
private Wiki enWiki = new Wiki("en.wikipedia.org");
// approved user lists
private final String[] approvedUsers = { "MER-C", "Jjron" };
// wizard
private JWizard wizard;
// text area for progress
private JTextArea textarea;
// blurb
private String blurb = " using [[User:MER-C/FPC Closing Wizard|FPC Closing Wizard]]";
public static void main(String[] args) throws IOException, LoginException
{
new FPC();
}
public FPC()
{
// initial wizard panel
Box intro = Box.createVerticalBox();
JLabel label = new JLabel("<html>This wizard closes featured picture candidacies on the English Wikipedia.<br><br>" +
"(C) 2008 MER-C. FPC Closing Wizard is free software, you may distribute it under the terms of the GNU " +
"General Public License version 3 or any subsequent version. Icon taken from [[Image:718smiley.svg]], by East718." +
"<br><br>This program comes with absolutely no warranty, therefore <font color=red>you are solely responsible " +
"for the edits you make with this program</font>.<br><br>Bugs, comments and approval requests belong at " +
"[[User talk:MER-C]].<br><br>Click Next to continue.</html>");
// init wizard, standard listeners
intro.add(label);
intro.add(Box.createVerticalGlue());
ActionListener next = new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
wizard.next();
}
};
ActionListener back = new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
wizard.back();
}
};
wizard = new JWizard(intro, next);
wizard.setWizardName("FPC Closing Wizard");
wizard.setShowingSplashScreen(false);
wizard.setPanelSubtitles(0, "Introduction");
// second login panel
JPanel loginPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
// destructions for this panel
gbc.anchor = GridBagConstraints.WEST;
gbc.gridwidth = 2;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.ipady = 20;
gbc.weightx = 0;
gbc.weighty = 0;
loginPanel.add(new JLabel("Enter your Wikipedia username and password."), gbc);
// username box
final JTextField username = new JTextField("");
username.setColumns(10);
username.setMaximumSize(username.getPreferredSize());
gbc.gridy = 1;
gbc.ipadx = 50;
gbc.ipady = 0;
gbc.gridwidth = 1;
loginPanel.add(new JLabel("Username:"), gbc);
gbc.gridx = 1;
loginPanel.add(username, gbc);
// password box
final JPasswordField password = new JPasswordField();
password.setColumns(10);
password.setMaximumSize(password.getPreferredSize());
gbc.gridx = 0;
gbc.gridy = 2;
loginPanel.add(new JLabel("Password:"), gbc);
gbc.gridx = 1;
loginPanel.add(password, gbc);
// more destructions
gbc.gridwidth = 2;
gbc.gridx = 0;
gbc.gridy = 3;
gbc.ipadx = 0;
gbc.ipady = 20;
loginPanel.add(new JLabel("Click Next to login and proceed."), gbc);
// padding
gbc.weighty = 100;
gbc.gridy = 4;
loginPanel.add(Box.createVerticalGlue(), gbc);
gbc.gridheight = 4;
gbc.gridx = 2;
gbc.gridy = 0;
gbc.weightx = 100;
gbc.weighty = 0;
loginPanel.add(Box.createHorizontalGlue(), gbc);
// add it
wizard.add(loginPanel, new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
try
{
// check for approval
String user = username.getText();
if (!Arrays.asList(approvedUsers).contains(user))
{
JOptionPane.showMessageDialog(null, "You are not approved to use this program. Please contact the author " +
"for approval.", "Access denied", JOptionPane.ERROR_MESSAGE);
}
// this really needs to go in another thread...
enWiki.login(user, password.getPassword());
enWiki.setMaxLag(-1);
enWiki.setThrottle(10000);
// change wizard
wizard.next();
wizard.setBackButtonEnabled(false);
}
catch (Exception ex)
{
JOptionPane.showMessageDialog(null, ex.toString(), "Login failed", JOptionPane.ERROR_MESSAGE);
}
}
}, back);
wizard.setPanelSubtitles(1, "Login");
// first input entry
Box tablePanel = Box.createVerticalBox();
// table entry destructions
tablePanel.add(new JLabel("<html>Enter the debates you want to close and their results in the table " +
"below. You do not have to fill out the last four columns for unsuccessful nominations. For the " +
"nomination title, exclude \"Wikipedia:Featured picture candidates/\". Leave the creator field blank " +
"if the creator is not a Wikipedian.</html>"));
tablePanel.add(Box.createVerticalStrut(20));
// table
final JTable table = new JTable(new FPCTableModel());
tablePanel.add(new JScrollPane(table));
// set combo box editor for outcomes
JComboBox outcomes = new JComboBox(new String[]
{
"Not promoted", "Promoted"
});
DefaultCellEditor editor = new DefaultCellEditor(outcomes);
TableColumnModel tcm = table.getColumnModel();
tcm.getColumn(1).setCellEditor(editor);
// set column widths
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
tcm.getColumn(0).setMinWidth(200);
tcm.getColumn(1).setMinWidth(100);
tcm.getColumn(2).setMinWidth(200);
tcm.getColumn(3).setMinWidth(100);
tcm.getColumn(4).setMinWidth(100);
tcm.getColumn(5).setMinWidth(200);
// final destructions
tablePanel.add(Box.createVerticalStrut(20));
tablePanel.add(new JLabel("Click Next to perform closing."));
// add it
wizard.add(tablePanel, new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
// build closing data
TableModel model = table.getModel();
ArrayList<String[]> temp = new ArrayList<String[]>();
for (int i = 0; i < model.getRowCount(); i++)
{
// prune empty rows
if (model.getValueAt(i, 0) == null)
continue;
temp.add(new String[]
{
"Wikipedia:Featured picture candidates/" + (String)model.getValueAt(i, 0), // nomination page
(String)model.getValueAt(i, 2), // image promoted
(String)model.getValueAt(i, 3), // nominator
(String)model.getValueAt(i, 4), // creator
(String)model.getValueAt(i, 5) // short caption
});
}
// perform closing
wizard.next();
new FPCClosingThread(temp.toArray(new String[0][])).start();
wizard.setBackButtonEnabled(false);
wizard.setNextButtonEnabled(false);
}
}, null);
wizard.setPanelSubtitles(2, "Enter debate results");
// progress panel
textarea = new JTextArea();
textarea.setEditable(false);
wizard.add(new JScrollPane(textarea), next, back);
wizard.setPanelSubtitles(3, "Closing in progress...");
// end panel
Box end = Box.createVerticalBox();
end.add(new JLabel("<html>Closing operations finished. Click Finish to exit. <br><br>" +
"<font color=red>Don't forget to remove {{FPC}} from not promoted images, update [[Wikipedia:Featured " +
"pictures]] and the relevant subgalleries</font> and if an edit was promoted replace the original with it " +
"in articles. If an error occurred, you'll have to complete the process manually.</html>"));
end.add(Box.createVerticalGlue());
wizard.add(end, new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
System.exit(0);
}
}, back);
wizard.setPanelSubtitles(4, "Closing complete");
// frame me
wizard.setPreferredSize(new Dimension(640, 480));
JFrame frame = wizard.createFrame();
ImageIcon icon = new ImageIcon(FPC.class.getResource("718smiley.png"));
frame.setIconImage(icon.getImage());
frame.addWindowListener(new WindowAdapter()
{
public void windowClosed(WindowEvent e)
{
System.exit(0);
}
});
frame.setVisible(true);
}
/**
* Fetches current nomination list. (This might be used later.)
* @return an array of open nomination page names
* @throws IOException if a network error occurs
*/
private String[] fetch() throws IOException
{
// exclude all the meta stuff
String[] excludes =
{
"Wikipedia:Featured picture candidates/Header",
"Wikipedia:Featured picture candidates/delistfooter",
"Wikipedia:Featured picture candidates/delistheader",
"Wikipedia:Featured picture candidates/Nomination procedure",
"Wikipedia:Featured picture candidates/Closing procedure"
};
return Wiki.relativeComplement(enWiki.getTemplates("Wikipedia:Featured picture candidates", Wiki.PROJECT_NAMESPACE), excludes);
}
/**
* Thread for closing nominations.
*/
class FPCClosingThread extends Thread
{
// closing data
private String[][] closedNoms;
/**
* @param closedNoms nomination data
*/
public FPCClosingThread(String[][] closedNoms)
{
this.closedNoms = closedNoms;
}
/**
* Runs this thread. Here we catch all the exceptions.
*/
public void run()
{
try
{
close();
}
catch (AccountLockedException ex)
{
// User blocked. Exit.
JOptionPane.showMessageDialog(null, "You are currently blocked from editing!", "User blocked", JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
catch (IOException ex)
{
// Usually a network error.
JOptionPane.showMessageDialog(null, "A network error has occurred: " + ex, "Error", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
wizard.next();
}
catch (LoginException ex)
{
// Shouldn't happen, none of these pages are fully protected
ex.printStackTrace();
}
}
/**
* Performs closing and associated operations.
* @throws IOException if a network error occurs
* @throws LoginException if blocked or a page is protected
*/
public void close() throws IOException, LoginException
{
// some stuff we need later
SimpleDateFormat format = new SimpleDateFormat("MMMM-yyyy");
ArrayList<String[]> pile2 = new ArrayList<String[]>();
for (int i = 0; i < closedNoms.length; i++)
pile2.add(closedNoms[i]);
Runnable reset = new Runnable()
{
public void run()
{
wizard.setNextButtonEnabled(true);
wizard.back();
}
};
// Check the nomination pile for nominations that don't exist.
// We want to do this before any edits because then we'd have to revert them.
textarea.append("Checking whether nominations exist... ");
ArrayList<String> temp = new ArrayList<String>(30);
for (int i = 0; i < closedNoms.length; i++)
temp.add(closedNoms[i][0]);
boolean[] exists = enWiki.exists(temp.toArray(new String[0]));
for (int i = 0; i < exists.length; i++)
{
if (!exists[i])
{
// Nomination doesn't exist. Move the wizard back to give the user the
// chance to correct typo.
JOptionPane.showMessageDialog(null, "Nomination " + closedNoms[i][0] + " does not exist!", "Input error", JOptionPane.ERROR_MESSAGE);
SwingUtilities.invokeLater(reset);
return;
}
}
textarea.append("done.\n");
// close and archive
String pile = enWiki.getPageText("Wikipedia:Featured picture candidates");
String archiveName = "Wikipedia:Featured picture candidates/" + format.format(new Date());
String archive = enWiki.getPageText(archiveName);
for (int i = 0; i < closedNoms.length; i++)
{
// close nomination
String nomination = "";
textarea.append("Closing [[" + closedNoms[i][0] + "]]... ");
nomination = enWiki.getPageText(closedNoms[i][0]);
// close the nomination: replace <!-- additional votes go above this line --> with {{FPCresult|...}}
int a = nomination.indexOf("<!-- additional votes go above this line -->");
int b = nomination.indexOf("\n", a);
if (a < 0)
{
// Malformed nomination. Give user a chance to fix it.
JOptionPane.showMessageDialog(null, "Nomination " + closedNoms[i][0] + " is malformed! Please fix it and revert any" +
" closures done so far.", "Malformed nomination", JOptionPane.ERROR_MESSAGE);
SwingUtilities.invokeLater(reset);
return;
}
// build the new page text
StringBuilder text = new StringBuilder(nomination.substring(0, a));
text.append("\n{{FPCresult|");
// deal with not promoted images (TESTED)
if (closedNoms[i][1] == null || closedNoms[i][1].equals(""))
{
text.append("Not promoted|}} ~~~~");
pile2.remove(closedNoms[i]); // remove for later overwriting
// // strip {{FPC|...}} from any nominated images
// String[] images = enWiki.getImagesOnPage(closedNoms[i][0]);
// boolean[] exists = enWiki.exists(images);
// for (int ii = 0; ii < images.length; ii++)
// {
// if (exists[ii])
// {
// String text2 = enWiki.getPageText(images[ii]);
// String text3 = text2.toUpperCase().toLowerCase();
// if (text3.contains("{{fpc"))
// {
// a = text3.indexOf("{{fpc");
// b = text3.indexOf("}}", a) + 2;
// String newText = text2.substring(0, a) + text2.substring(b);
// if (newText.trim().equals(""))
// // tag as empty description page
// enWiki.edit(images[ii], "{{db-g8}}", "Tagging empty description page" + blurb, false);
// else
// // remove {{fpc}}
// enWiki.edit(images[ii], newText, "-{{fpc}}" + blurb, false);
// }
// }
// }
}
else // promoted (TESTED)
{
text.append("Promoted|");
text.append(closedNoms[i][1]);
text.append("}} ~~~~");
}
text.append(nomination.substring(b));
enWiki.edit(closedNoms[i][0], text.toString(), "Closed" + blurb, false);
textarea.append("done.\n");
// move the nomination from the main page (string) to the archive (string)
a = pile.indexOf(closedNoms[i][0]) - 2;
b = pile.indexOf("}}", a) + 3;
pile = pile.substring(0, a) + pile.substring(b);
archive += ("{{" + closedNoms[i][0] + "}}\n");
}
// perform the archiving (TESTED)
textarea.append("Archiving nominations... ");
enWiki.edit("Wikipedia:Featured picture candidates", pile, "-" + closedNoms.length + blurb, false);
enWiki.edit(archiveName, archive, "+" + closedNoms.length + blurb, false);
textarea.append("done\n");
// overwrite closedNoms to include only promoted images
closedNoms = pile2.toArray(new String[0][]);
// For each of the promoted pictures...
for (int i = 0; i < closedNoms.length; i++)
{
// ... notify the creator... (TESTED)
textarea.append("Notifying creator/nominator of [[" + closedNoms[i][1] + "]]... ");
if (closedNoms[i][3] != null)
enWiki.newSection("User talk:" + closedNoms[i][3], "[[" + closedNoms[i][0] + "]]", "{{subst:uploadedFP|" + closedNoms[i][1] + "}}", false);
// ... and the nominator (but only if they weren't the creator)...
if (!closedNoms[i][2].equals(closedNoms[i][3]))
enWiki.newSection("User talk:" + closedNoms[i][2], "[[" + closedNoms[i][0] + "]]", "{{subst:promotedFPC|" + closedNoms[i][1] + "}}", false);
textarea.append("done.\n");
//... tag it as featured... (TESTED)
textarea.append("Tagging [[" + closedNoms[i][1] + "]] as featured... ");
String description = "";
try
{
description = enWiki.getPageText(closedNoms[i][1]);
}
catch (FileNotFoundException ex)
{
// Page doesn't exist. Set description to some dummy string.
description = "{{FPC}}";
}
String description2 = description.toUpperCase().toLowerCase();
String trimmedDebateName = closedNoms[i][0].substring(38); // enough to lop off the prefix
if (description2.contains("{{fpc"))
{
// replace {{fpc|...}} with {{FeaturedPicture|...}}
int a = description2.indexOf("{{fpc");
int b = description2.indexOf("}}", a) + 2;
enWiki.edit(closedNoms[i][1], description.substring(0, a) + "{{FeaturedPicture|" + trimmedDebateName + "}}\n" + description.substring(b), "Featured" + blurb, false);
}
else
enWiki.edit(closedNoms[i][1], "{{FeaturedPicture|" + trimmedDebateName + "}}\n" + description, "Featured" + blurb, false);
textarea.append("done.\n");
}
// update [[Template:Announcements/New featured content]] (TESTED)
textarea.append("Updating [[Template:Announcements/New featured content]]... ");
String nfc = enWiki.getPageText("Template:Announcements/New featured content");
int a = nfc.indexOf("<!-- Pictures (15, most recent first) -->\n") + 42;
int b = nfc.indexOf("\n\n", a);
String[] list = nfc.substring(a, b).split("\n");
StringBuilder newNFC = new StringBuilder(nfc.substring(0, a));
for (int i = 0; i < 15; i++)
{
if (i < closedNoms.length)
{
// list format: * [[:Image:Example.png|Example image]]
newNFC.append("* [[:");
newNFC.append(closedNoms[i][1]);
newNFC.append("|");
newNFC.append(closedNoms[i][4]);
newNFC.append("]]");
newNFC.append("\n");
}
// add the rest, barring those on the bottom
else
{
newNFC.append(list[i - closedNoms.length]);
if (i != 14)
newNFC.append("\n");
}
}
newNFC.append(nfc.substring(b));
enWiki.edit("Template:Announcements/New featured content", newNFC.toString(), "Rotating new FPs" + blurb, false);
textarea.append("done.\n");
// update [[Wikipedia:Goings-on]] (TESTED)
textarea.append("Updating [[Wikipedia:Goings-on]]... ");
String goingsOn = enWiki.getPageText("Wikipedia:Goings-on");
a = goingsOn.indexOf("'''[[Wikipedia:Featured pictures|Pictures]] that gained featured status'''") + 75;
StringBuilder newGoings = new StringBuilder(goingsOn.substring(0, a));
format.applyPattern("MMMM d"); // reuse old date format
for (int i = 0; i < closedNoms.length; i++)
{
// list format: * [[:Image:Example.png|Example image]] (April 1)
newGoings.append("* [[:");
newGoings.append(closedNoms[i][1]);
newGoings.append("|");
newGoings.append(closedNoms[i][4]);
newGoings.append("]] (");
newGoings.append(format.format(new Date()));
newGoings.append(")\n");
}
newGoings.append(goingsOn.substring(a));
enWiki.edit("Wikipedia:Goings-on", newGoings.toString(), "+" + closedNoms.length + blurb, false);
textarea.append("done.\n");
// update [[Wikipedia:Featured picture thumbs]] (TESTED)
// resolve fpt redirect
textarea.append("Adding pics to [[Wikipedia:Featured picture thumbs]]... ");
String fpt = enWiki.getPageText("Wikipedia:Featured pictures thumbs");
a = fpt.indexOf("[[") + 2;
b = fpt.indexOf("]]");
String galleryName = fpt.substring(a, b);
// perform addition
fpt = enWiki.getPageText(galleryName);
a = fpt.indexOf("<gallery>\n") + 10;
StringBuilder fptNewText = new StringBuilder(fpt.substring(0, a));
for (int i = 0; i < closedNoms.length; i++)
{
// list format: Image:Example.png|Example image
fptNewText.append(closedNoms[i][1]);
fptNewText.append("|");
fptNewText.append(closedNoms[i][4]);
fptNewText.append("\n");
}
fptNewText.append(fpt.substring(a));
enWiki.edit(galleryName, fptNewText.toString(), "+" + closedNoms.length + blurb, false);
textarea.append("done.\n");
// done
enWiki.logoutServerSide();
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
textarea.append("All is OK.\n\n");
textarea.append("Stuff to copy/paste:\n");
for (int i = 0; i < closedNoms.length; i++)
{
// copypasta for FP and subgalleries
// form: |[[Image:Example.png|150px]] and Image:Example.png|caption by X
textarea.append("|[[");
textarea.append(closedNoms[i][1]);
textarea.append("|150px]]\n");
textarea.append(closedNoms[i][1]);
textarea.append("|");
textarea.append(closedNoms[i][4]);
textarea.append(", by\n");
}
wizard.setNextButtonEnabled(true);
}
});
}
}
/**
* Table model for the table where we enter closing and image data.
*/
class FPCTableModel extends DefaultTableModel
{
private String[] columnNames =
{
"Nomination page", "Outcome", "Image promoted", "Nominator", "Creator", "Short caption"
};
/** { @inheritdoc } */
public String getColumnName(int column)
{
return columnNames[column];
}
/** { @inheritdoc } */
public boolean isCellEditable(int row, int column)
{
return true;
}
/** { @inheritdoc } */
public int getColumnCount()
{
return 6;
}
/** { @inheritdoc } */
public int getRowCount()
{
// shouldn't have to close more than 20 debates in one go...
return 20;
}
/**
* Override getValueAt() to automatically trim().
*/
public Object getValueAt(int row, int column)
{
// we only put strings in this table
String value = (String)super.getValueAt(row, column);
// convert empty strings to null
if (value == null || value.equals(""))
return null;
return value.trim();
}
}
}