Jump to content

User:MER-C/FPC.java

From Wikipedia, the free encyclopedia
/**
 *  @(#)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();
        }
    }
}