User:HersfoldBot/Source

This page contains a copy of the code used to run User:HersfoldBot. This will be updated to reflect changes made to the code, which will be summarized at ../Version.

Wiki.java
The bot uses User:MER-C/Wiki.java, however I've partially modified it for compatibility and bug fixing. The version I use is below.

*        *  Note that the duration of a block may be given as a period of time * (e.g. "31 hours") or a timestamp (e.g. 20071216160302). To tell * these apart, feed it into Long.parseLong and catch any * resulting exceptions. *        *  @return the details of the log entry * @since 0.08 */       public Object getDetails {           return details; }

/**        *  Returns a string representation of this log entry. * @return a string representation of this object * @since 0.08 */       public String toString {           // @revised 0.17 to a more traditional Java approach StringBuilder s = new StringBuilder("LogEntry[type="); s.append(type); s.append(",action="); s.append(action == null ? "[hidden]" : action); s.append(",user="); s.append(user == null ? "[hidden]" : user.getUsername); s.append(",timestamp="); s.append(calendarToTimestamp(timestamp)); s.append(",target="); s.append(target == null ? "[hidden]" : target); s.append(",reason="); s.append(reason == null ? "[hidden]" : reason); s.append(",details="); if (details instanceof Object[]) s.append(Arrays.asList((Object[])details)); // crude formatting hack else s.append(details); s.append("]"); return s.toString; }

/**        *  Compares this log entry to another one based on the recentness * of their timestamps. * @param other the log entry to compare * @return whether this object is equal to         *  @since 0.18 */       public int compareTo(Wiki.LogEntry other) {           if (timestamp.equals(other.timestamp)) return 0; // might not happen, but return timestamp.after(other.timestamp) ? 1 : -1;       }    }

/**    *  Represents a contribution and/or a revision to a page. * @since 0.17 */   public class Revision implements Comparable {       private boolean minor; private String summary; private long revid, rcid = -1; private Calendar timestamp; private String user; private String title;

/**        *  Constructs a new Revision object. * @param revid the id of the revision (this is a long since         *   on en.wikipedia.org is now (November 2007) ~10%         *  of Integer.MAX_VALUE         *  @param timestamp when this revision was made         *  @param article the concerned article         *  @param summary the edit summary         *  @param user the user making this revision (may be anonymous, if not * use User.getUsername)         *  @param minor whether this was a minor edit         *  @since 0.17         */        protected Revision(long revid, Calendar timestamp, String title, String summary, String user, boolean minor)        {            this.revid = revid;            this.timestamp = timestamp;            this.summary = summary;            this.minor = minor;            this.user = user;			this.title = title;        }

/**        *  Fetches the contents of this revision. WARNING: fails if the * revision is deleted. * @return the contents of the appropriate article at timestamp * @throws IOException if a network error occurs * @throws IllegalArgumentException if page == Special:Log/xxx. * @since 0.17 */       public String getText throws IOException {           // logs have no content if (revid == -1L) throw new IllegalArgumentException("Log entries have no valid content!");

// go for it           String url = base + URLEncoder.encode(title, "UTF-8") + "&oldid=" + revid + "&action=raw"; String temp = fetch(url, "Revision.getText", false); log(Level.INFO, "Successfully retrieved text of revision " + revid, "Revision.getText"); return decode(temp); }

/**        *  Gets the rendered text of this revision. WARNING: fails if the * revision is deleted. * @return the rendered contents of the appropriate article at         *  timestamp * @throws IOException if a network error occurs * @throws IllegalArgumentException if page == Special:Log/xxx. * @since 0.17 */       public String getRenderedText throws IOException {           // logs have no content if (revid == -1L) throw new IllegalArgumentException("Log entries have no valid content!");

// go for it           String url = base + URLEncoder.encode(title, "UTF-8") + "&oldid=" + revid + "&action=render"; String temp = fetch(url, "Revision.getRenderedText", false); log(Level.INFO, "Successfully retrieved rendered text of revision " + revid, "Revision.getRenderedText"); return decode(temp); }

/**        *  Determines whether this Revision is the most recent revision of         *  the relevant page. *        *  @return see above * @throws IOException if a network error occurs * @since 0.17 */       public boolean isTop throws IOException {           String url = query + "action=query&prop=revisions&titles=" + URLEncoder.encode(title, "UTF-8") + "&rvlimit=1&rvprop=timestamp|ids"; String line = fetch(url, "Revision.isTop", false); // fetch the oldid int a = line.indexOf("revid=\"") + 7;           int b = line.indexOf("\"", a); long oldid2 = Long.parseLong(line.substring(a, b)); return revid == oldid2; }

/**        *  Returns a HTML rendered diff between this and the specified * revision. Such revisions should be on the same page. * @param other another revision on the same page. NEXT_REVISION, * PREVIOUS_REVISION and CURRENT_REVISION can be used here for obvious * effect. * @return the difference between this and the other revision. See * http://en.wikipedia.org/w/index.php?diff=343490272 for an example. * @throws IOException if a network error occurs * @since 0.21 */       public String diff(Revision other) throws IOException {           return diff(other.revid, ""); }

/**        *  Returns a HTML rendered diff between this revision and the given * text. Useful for emulating the "show changes" functionality. * @param text some wikitext * @return the difference between this and the the text provided * @throws IOException if a network error occurs * @since 0.21 */       public String diff(String text) throws IOException {           return diff(0L, text); }

/**        *  Fetches a HTML rendered diff. * @param oldid the id of another revision; (exclusive) or         *  @param text some wikitext to compare agains * @return a difference between oldid or text * @throws IOException if a network error occurs * @since 0.21 */       protected String diff(long oldid, String text) throws IOException {           // send via POST URLConnection connection = new URL(query).openConnection; logurl(query, "Revision.diff"); setCookies(connection, cookies); connection.setDoOutput(true); connection.connect; PrintWriter out = new PrintWriter(connection.getOutputStream); out.write("action=query&prop=revisions&revids=" + revid);

// no switch for longs? WTF? if (oldid == NEXT_REVISION) out.write("&rvdiffto=next"); else if (oldid == CURRENT_REVISION) out.write("&rvdiffto=cur"); else if (oldid == PREVIOUS_REVISION) out.write("&rvdiffto=previous"); else if (oldid == 0L) {               out.write("&rvdifftotext="); out.write(text); }           else {               out.write("&rvdiffto="); out.write("" + oldid); }           out.close;

// parse BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream), "UTF-8")); String line; StringBuilder diff = new StringBuilder(100000); while ((line = in.readLine) != null) {               int y = line.indexOf(">", line.indexOf("<diff")) + 1; int z = line.indexOf(" "); if (y != -1) {                   diff.append(line.substring(y + 6)); diff.append("\n"); }               else if (z != -1) {                   diff.append(line.substring(0, z)); diff.append("\n"); break; // done }               else {                   diff.append(line); diff.append("\n"); }           }            return decode(diff.toString); }

/**        *  Determines whether this Revision is equal to another object. * @param o an object * @return whether o is equal to this object * @since 0.17 */       public boolean equals(Object o)        { if (!(o instanceof Revision)) return false; return toString.equals(o.toString); }

/**        *  Returns a hash code of this revision. * @return a hash code * @since 0.17 */       public int hashCode {           return (int)revid * 2 - Wiki.this.hashCode; }

/**        *  Checks whether this edit was marked as minor. See * Help:Minor edit for details. *        *  @return whether this revision was marked as minor * @since 0.17 */       public boolean isMinor {           return minor; }

/**        *  Returns the edit summary for this revision. WARNING: returns null * if the summary was RevisionDeleted. * @return the edit summary * @since 0.17 */       public String getSummary {           return summary; }

/**        *  Returns the user or anon who created this revision. You should * pass this (if not an IP) to getUser(String) to obtain a         *  User object. WARNING: returns null if the user was RevisionDeleted. * @return the user or anon * @since 0.17 */       public String getUser {           return user; }

/**        *  Returns the page to which this revision was made. * @return the page * @since 0.17 */       public String getPage {           return title; }

/**        *  Returns the oldid of this revision. Don't confuse this with * rcid * @return the oldid (long) * @since 0.17 */       public long getRevid {           return revid; }

/**        *  Gets the time that this revision was made. * @return the timestamp * @since 0.17 */       public Calendar getTimestamp {           return timestamp; }

/**        *  Returns a string representation of this revision. * @return see above * @since 0.17 */       public String toString {           StringBuilder sb = new StringBuilder("Revision[oldid="); sb.append(revid); sb.append(",page=\"");           sb.append(title);            sb.append("\",user="); sb.append(user == null ? "[hidden]" : user); sb.append(",timestamp="); sb.append(calendarToTimestamp(timestamp)); sb.append(",summary=\"");           sb.append(summary == null ? "[hidden]" : summary);            sb.append("\",minor="); sb.append(minor); sb.append(",rcid="); sb.append(rcid == -1 ? "unset" : rcid); sb.append("]"); return sb.toString; }

/**        *  Compares this revision to another revision based on the recentness * of their timestamps. * @param other the revision to compare * @return whether this object is equal to         *  @since 0.18 */       public int compareTo(Wiki.Revision other) {           if (timestamp.equals(other.timestamp)) return 0; // might not happen, but return timestamp.after(other.timestamp) ? 1 : -1;       }

/**        *  Sets the rcid</tt> of this revision, used for patrolling. * This parameter is optional. I can't think of a good reason why * this should be publicly editable. * @param rcid the rcid of this revision (long) * @since 0.17 */       protected void setRcid(long rcid) {           this.rcid = rcid; }

/**        *  Gets the <tt>rcid</tt> of this revision for patrolling purposes. * @return the rcid of this revision (long) * @since 0.17 */       public long getRcid {           return rcid; }

/**        *  Reverts this revision using the rollback method. See * <tt>Wiki.rollback</tt>. * @throws IOException if a network error occurs * @throws CredentialNotFoundException if not logged in or user is not * an admin * @throws AccountLockedException if the user is blocked * @since 0.19 */       public void rollback throws IOException, LoginException {           Wiki.this.rollback(this, false, ""); }

/**        *  Reverts this revision using the rollback method. See * <tt>Wiki.rollback</tt>. * @param bot mark this and the reverted revision(s) as bot edits * @param reason (optional) a custom reason * @throws IOException if a network error occurs * @throws CredentialNotFoundException if not logged in or user is not * an admin * @throws AccountLockedException if the user is blocked * @since 0.19 */       public void rollback(boolean bot, String reason) throws IOException, LoginException {           Wiki.this.rollback(this, bot, reason); }   }

// INTERNALS

// miscellany

/**    *  A generic URL content fetcher. This is only useful for GET requests, * which is almost everything that doesn't modify the wiki. Might be    *  useful for subclasses. *    *  Here we also check the database lag and wait 30s if it exceeds * <tt>maxlag</tt>. See mw:Manual:Maxlag parameter for the server-side * analog (which isn't implemented here, because I'm too lazy to retry     *  the request). *    *  @param url the url to fetch * @param caller the caller of this method * @param write whether we need to fetch the cookies from this connection * in a token-fetching exercise (edit and friends) * @throws IOException if a network error occurs * @since 0.18 */   protected String fetch(String url, String caller, boolean write) throws IOException {       // check the database lag logurl(url, caller); do // this is just a dummy loop {           if (maxlag < 1) // disabled break; // only bother to check every 30 seconds if ((System.currentTimeMillis - lastlagcheck) < 30000) // TODO: this really should be a preference break;

try {               // if we use this, this can block unrelated read requests while we edit a page synchronized(domain) {                   // update counter. We do this before the actual check, so that only one thread does the check. lastlagcheck = System.currentTimeMillis; int lag = getCurrentDatabaseLag; while (lag > maxlag) {                       log(Level.WARNING, "Sleeping for 30s as current database lag exceeds the maximum allowed value of " + maxlag + " s", caller); Thread.sleep(30000); lag = getCurrentDatabaseLag; }               }            }            catch (InterruptedException ex) {               // nobody cares }       }        while (false);

// connect URLConnection connection = new URL(url).openConnection; setCookies(connection, cookies); connection.connect; BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream), "UTF-8"));

// get the cookies if (write) {           grabCookies(connection, cookies2); cookies2.putAll(cookies); }

// get the text String line; StringBuilder text = new StringBuilder(100000); while ((line = in.readLine) != null) {           text.append(line); text.append("\n"); }       in.close; return text.toString; }

/**    *  Checks for errors from standard read/write requests. * @param line the response from the server to analyze * @param caller what we tried to do     *  @throws AccountLockedException if the user is blocked * @throws HttpRetryException if the database is locked or action was * throttled and a retry failed * @throws UnknownError in the case of a MediaWiki bug * @since 0.18 */   protected void checkErrors(String line, String caller) throws IOException, LoginException {       // System.out.println(line); // empty response from server if (line.equals("")) throw new UnknownError("Received empty response from server!"); // successful if (line.contains("result=\"Success\"")) return; // rate limit (automatic retry), though might be a long one (e.g. email) if (line.contains("error code=\"ratelimited\"")) {           log(Level.WARNING, "Server-side throttle hit.", caller); throw new HttpRetryException("Action throttled.", 503); }       // blocked! if (line.contains("error code=\"blocked") || line.contains("error code=\"autoblocked\""))       {            log(Level.SEVERE, "Cannot " + caller + " - user is blocked!.", caller);            throw new AccountLockedException("Current user is blocked!");        }        // cascade protected        if (line.contains("error code=\"cascadeprotected\""))        {            log(Level.WARNING, "Cannot " + caller + " - page is subject to cascading protection.", caller);            throw new CredentialException("Page is cascade protected");        }        // database lock (automatic retry)        if (line.contains("error code=\"readonly\""))        {            log(Level.WARNING, "Database locked!", caller);            throw new HttpRetryException("Database locked!", 503);        }        // unknown error        if (line.contains("error code=\"unknownerror\""))            throw new UnknownError("Unknown MediaWiki API error, response was " + line); // generic (automatic retry) throw new IOException("MediaWiki error, response was " + line); }

/**    *  Strips entity references like &quot; from the supplied string. This * might be useful for subclasses. * @param in the string to remove URL encoding from * @return that string without URL encoding * @since 0.11 */   protected String decode(String in) {       // Remove entity references. Oddly enough, URLDecoder doesn't nuke these. in = in.replace("&lt;", "<").replace("&gt;", ">"); // html tags in = in.replace("&amp;", "&"); in = in.replace("&quot;", "\"");       in = in.replace("&#039;", "'");        return in;    }

/**    *  Finalizes the object on garbage collection. * @since 0.14 */   protected void finalize {       // I have no idea why this is called when we are still using // this Wiki object. Silly Java. //       Thread.dumpStack; //       logout; //       namespaces = null; }

// user rights methods

/**    *  Checks whether the currently logged on user has sufficient rights to     *  edit/move a protected page. *    *  @param level a protection level * @param move whether the action is a move * @return whether the user can perform the specified action * @throws IOException if we can't get the user rights * @throws AccountLockedException if user is blocked * @throws AssertionError if any defined assertions are false * @since 0.10 */   private boolean checkRights(int level, boolean move) throws IOException, AccountLockedException {       // admins can do anything, this also covers FULL_PROTECTION if ((user.userRights & ADMIN) == ADMIN) return true; switch (level) {           case NO_PROTECTION: return true; case SEMI_PROTECTION: return user != null; // not logged in => can't edit case MOVE_PROTECTION: case SEMI_AND_MOVE_PROTECTION: return !move; // fall through is OK: the user cannot move a protected page // cases PROTECTED_DELETED_PAGE and FULL_PROTECTION are unnecessary default: return false; }   }

/**    *  Performs a status check, including assertions. * @throws AssertionError if any assertions are false * @throws AccountLockedException if the user is blocked * @throws IOException if a network error occurs * @see #setAssertionMode * @since 0.11 */   protected void statusCheck throws IOException, AccountLockedException {       // @revised 0.18 was assertions, put some more stuff in here

// check if MediaWiki hasn't logged us out if (cookies != null && !cookies.containsValue(user.getUsername)) {           log(Level.WARNING, "Cookies expired", "statusCheck"); logout; }

// perform various status checks every 100 or so edits if (statuscounter > statusinterval) {           // purge user rights in case of desysop or loss of other priviliges if (user != null) user.userRights(false); // check for new messages if ((assertion & ASSERT_NO_MESSAGES) == ASSERT_NO_MESSAGES) assert !(hasNewMessages) : "User has new messages";

statuscounter = 0; }       else statuscounter++;

// do some more assertions if ((assertion & ASSERT_LOGGED_IN) == ASSERT_LOGGED_IN) assert (user != null) : "Not logged in"; if ((assertion & ASSERT_BOT) == ASSERT_BOT) assert (user.userRights & BOT) == BOT : "Not a bot"; }

// cookie methods

/**    *  Sets cookies to an unconnected URLConnection and enables gzip * compression of returned text. * @param u an unconnected URLConnection * @param map the cookie store */   private void setCookies(URLConnection u, Map<String, String> map) {       StringBuilder cookie = new StringBuilder(100); for (Map.Entry<String, String> entry : map.entrySet) {           cookie.append(entry.getKey); cookie.append("="); cookie.append(entry.getValue); cookie.append("; "); }       u.setRequestProperty("Cookie", cookie.toString);

// enable gzip compression u.setRequestProperty("Accept-encoding", "gzip"); u.setRequestProperty("User-Agent", useragent); }

/**    *  Grabs cookies from the URL connection provided. * @param u an unconnected URLConnection * @param map the cookie store */   private void grabCookies(URLConnection u, Map<String, String> map) {       // reset the cookie store map.clear; String headerName = null; for (int i = 1; (headerName = u.getHeaderFieldKey(i)) != null; i++) if (headerName.equals("Set-Cookie")) {               String cookie = u.getHeaderField(i);

// _session cookies are for cookies2, otherwise this causes problems if (cookie.contains("_session") && map == cookies) continue;

cookie = cookie.substring(0, cookie.indexOf(";")); String name = cookie.substring(0, cookie.indexOf("=")); String value = cookie.substring(cookie.indexOf("=") + 1, cookie.length); map.put(name, value); }   }

// logging methods

/**    *  Logs a successful result. * @param text string the string to log * @param method what we are currently doing * @param level the level to log at     *  @since 0.06 */   private void log(Level level, String text, String method) {       StringBuilder sb = new StringBuilder(100); sb.append('['); sb.append(domain); sb.append("] "); sb.append(text); sb.append('.'); logger.logp(level, "Wiki", method + "", sb.toString); }

/**    *  Logs a url fetch. * @param url the url we are fetching * @param method what we are currently doing * @since 0.08 */   private void logurl(String url, String method) {       logger.logp(Level.FINE, "Wiki", method + "", "Fetching URL " + url); }

// calendar/timestamp methods

/**    *  Turns a calendar into a timestamp of the format yyyymmddhhmmss. Might * be useful for subclasses. * @param c the calendar to convert * @return the converted calendar * @see #timestampToCalendar * @since 0.08 */   protected final String calendarToTimestamp(Calendar c)    { StringBuilder x = new StringBuilder; x.append(c.get(Calendar.YEAR)); int i = c.get(Calendar.MONTH) + 1; // January == 0! if (i < 10) x.append("0"); // add a zero if required x.append(i); i = c.get(Calendar.DATE); if (i < 10) x.append("0"); x.append(i); i = c.get(Calendar.HOUR_OF_DAY); if (i < 10) x.append("0"); x.append(i); i = c.get(Calendar.MINUTE); if (i < 10) x.append("0"); x.append(i); i = c.get(Calendar.SECOND); if (i < 10) x.append("0"); x.append(i); return x.toString; }

/**    *  Turns a timestamp of the format yyyymmddhhmmss into a Calendar object. * Might be useful for subclasses. *    *  @param timestamp the timestamp to convert * @return the converted Calendar * @see #calendarToTimestamp * @since 0.08 */   protected final Calendar timestampToCalendar(String timestamp) {       GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); int year = Integer.parseInt(timestamp.substring(0, 4)); int month = Integer.parseInt(timestamp.substring(4, 6)) - 1; // January == 0! int day = Integer.parseInt(timestamp.substring(6, 8)); int hour = Integer.parseInt(timestamp.substring(8, 10)); int minute = Integer.parseInt(timestamp.substring(10, 12)); int second = Integer.parseInt(timestamp.substring(12, 14)); calendar.set(year, month, day, hour, minute, second); return calendar; }

/**    *  Converts a timestamp of the form used by the API * (yyyy-mm-ddThh:mm:ssZ) to the form * yyyymmddhhmmss, which can be fed into <tt>timestampToCalendar</tt>. *    *  @param timestamp the timestamp to convert * @return the converted timestamp * @see #timestampToCalendar * @since 0.12 */   private String convertTimestamp(String timestamp) {       StringBuilder ts = new StringBuilder(timestamp.substring(0, 4)); ts.append(timestamp.substring(5, 7)); ts.append(timestamp.substring(8, 10)); ts.append(timestamp.substring(11, 13)); ts.append(timestamp.substring(14, 16)); ts.append(timestamp.substring(17, 19)); return ts.toString; }

// serialization

/**    *  Writes this wiki to a file. * @param out an ObjectOutputStream to write to     *  @throws IOException if there are local IO problems * @since 0.10 */   private void writeObject(ObjectOutputStream out) throws IOException {       out.writeObject(user.getUsername); out.writeObject(cookies); out.writeInt(throttle); out.writeInt(maxlag); out.writeInt(assertion); out.writeObject(scriptPath); out.writeObject(domain); out.writeObject(namespaces); out.write(statusinterval); out.writeObject(useragent); }

/**    *  Reads a copy of a wiki from a file. * @param in an ObjectInputStream to read from * @throws IOException if there are local IO problems * @throws ClassNotFoundException if we can't recognize the input * @since 0.10 */   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {       String z = (String)in.readObject; user = new User(z); cookies = (HashMap<String, String>)in.readObject; throttle = in.readInt; maxlag = in.readInt; assertion = in.readInt; scriptPath = (String)in.readObject; domain = (String)in.readObject; namespaces = (HashMap<String, Integer>)in.readObject; statusinterval = in.readInt; useragent = (String)in.readObject;

// various other intializations cookies2 = new HashMap<String, String>(10); base = "http://" + domain + scriptPath + "/index.php?title="; query = "http://" + domain + scriptPath + "/api.php?format=xml&";

// force a status check on next edit statuscounter = statusinterval; } }