package hudson.plugins.twitter; import hudson.Extension; import hudson.Functions; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Result; import hudson.model.User; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Mailer; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.json.JSONObject; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.GetMethod; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import twitter4j.AsyncTwitter; import twitter4j.Status; import twitter4j.TwitterAdapter; import twitter4j.TwitterException; /** * @author cactusman * @author justinedelson */ public class TwitterPublisher extends Notifier { private static final List<String> VALUES_REPLACED_WITH_NULL = Arrays.asList("", "(Default)", "(System Default)"); private static final Logger LOGGER = Logger.getLogger(TwitterPublisher.class.getName()); private String id; private String password; private Boolean onlyOnFailureOrRecovery; private Boolean includeUrl; private TwitterPublisher(String id, String password, Boolean onlyOnFailureOrRecovery, Boolean includeUrl) { this.onlyOnFailureOrRecovery = onlyOnFailureOrRecovery; this.includeUrl = includeUrl; this.id = id; this.password = password; } @DataBoundConstructor public TwitterPublisher(String id, String password, String onlyOnFailureOrRecovery, String includeUrl) { this(cleanToString(id), cleanToString(password), cleanToBoolean(onlyOnFailureOrRecovery), cleanToBoolean(includeUrl)); } private static String cleanToString(String string) { return VALUES_REPLACED_WITH_NULL.contains(string) ? null : string; } private static Boolean cleanToBoolean(String string) { Boolean result = null; if ("true".equals(string) || "Yes".equals(string)) { result = Boolean.TRUE; } else if ("false".equals(string) || "No".equals(string)) { result = Boolean.FALSE; } return result; } private static String createTinyUrl(String url) throws IOException { HttpClient client = new HttpClient(); GetMethod gm = new GetMethod("http://tinyurl.com/api-create.php?url=" + url.replace(" ", "%20")); int status = client.executeMethod(gm); if (status == HttpStatus.SC_OK) { return gm.getResponseBodyAsString(); } else { throw new IOException("Non-OK response code back from tinyurl: " + status); } } public String getId() { return id; } public Boolean getIncludeUrl() { return includeUrl; } public Boolean getOnlyOnFailureOrRecovery() { return onlyOnFailureOrRecovery; } public String getPassword() { return password; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { if (shouldTweet(build)) { try { String newStatus = createTwitterStatusMessage(build); ((DescriptorImpl) getDescriptor()).updateTwit(id, password, newStatus); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Unable to send tweet.", e); } } return true; } private String createTwitterStatusMessage(AbstractBuild<?, ?> build) { String projectName = build.getProject().getName(); String result = build.getResult().toString(); String toblame = ""; try { if (!build.getResult().equals(Result.SUCCESS)) { toblame = getUserString(build); } } catch (Exception e) { } String tinyUrl = ""; if (shouldIncludeUrl()) { String absoluteBuildURL = ((DescriptorImpl) getDescriptor()).getUrl() + build.getUrl(); try { tinyUrl = createTinyUrl(absoluteBuildURL); } catch (Exception e) { tinyUrl = "?"; } } return String.format("%s%s:%s $%d - %s", toblame, result, projectName, build.number, tinyUrl); } private String getUserString(AbstractBuild<?, ?> build) throws IOException { StringBuilder userString = new StringBuilder(""); Set<User> culprits = build.getCulprits(); ChangeLogSet<? extends Entry> changeSet = build.getChangeSet(); if (culprits.size() > 0) { for (User user : culprits) { UserTwitterProperty tid = user.getProperty(UserTwitterProperty.class); if (tid.getTwitterid() != null) { userString.append("@").append(tid.getTwitterid()).append(" "); } } } else if (changeSet != null) { for (Entry entry : changeSet) { User user = entry.getAuthor(); UserTwitterProperty tid = user.getProperty(UserTwitterProperty.class); if (tid.getTwitterid() != null) { userString.append("@").append(tid.getTwitterid()).append(" "); } } } return userString.toString(); } /** * Determine if this build represents a failure or recovery. A build failure * includes both failed and unstable builds. A recovery is defined as a * successful build that follows a build that was not successful. Always * returns false for aborted builds. * * @param build the Build object * @return true if this build represents a recovery or failure */ protected boolean isFailureOrRecovery(AbstractBuild<?, ?> build) { if (build.getResult() == Result.FAILURE || build.getResult() == Result.UNSTABLE) { return true; } else if (build.getResult() == Result.SUCCESS) { AbstractBuild<?, ?> previousBuild = build.getPreviousBuild(); if (previousBuild != null && previousBuild.getResult() != Result.SUCCESS) { return true; } else { return false; } } else { return false; } } protected boolean shouldIncludeUrl() { if (includeUrl != null) { return includeUrl.booleanValue(); } else { return ((DescriptorImpl) getDescriptor()).includeUrl; } } /** * Determine if this build results should be tweeted. Uses the local * settings if they are provided, otherwise the global settings. * * @param build the Build object * @return true if we should tweet this build result */ protected boolean shouldTweet(AbstractBuild<?, ?> build) { if (onlyOnFailureOrRecovery == null) { if (((DescriptorImpl) getDescriptor()).onlyOnFailureOrRecovery) { return isFailureOrRecovery(build); } else { return true; } } else if (onlyOnFailureOrRecovery.booleanValue()) { return isFailureOrRecovery(build); } else { return true; } } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { private static final Logger LOGGER = Logger.getLogger(DescriptorImpl.class.getName()); public String id; public String password; public String hudsonUrl; public boolean onlyOnFailureOrRecovery; public boolean includeUrl; public DescriptorImpl() { super(TwitterPublisher.class); load(); } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { // set the booleans to false as defaults includeUrl = false; onlyOnFailureOrRecovery = false; req.bindParameters(this, "twitter."); hudsonUrl = Mailer.descriptor().getUrl(); save(); return super.configure(req, formData); } @Override public String getDisplayName() { return "Twitter"; } public String getId() { return id; } public String getPassword() { return password; } public String getUrl() { return hudsonUrl; } public boolean isIncludeUrl() { return includeUrl; } public boolean isOnlyOnFailureOrRecovery() { return onlyOnFailureOrRecovery; } @SuppressWarnings("unchecked") @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException { if (hudsonUrl == null) { // if Hudson URL is not configured yet, infer some default hudsonUrl = Functions.inferHudsonURL(req); save(); } return super.newInstance(req, formData); } public void updateTwit(String id, String password, String message) throws Exception { if (id == null || password == null) { id = this.id; password = this.password; } LOGGER.info("Attempting to update Twitter status to: " + message); AsyncTwitter twitter = new AsyncTwitter(id,password); twitter.updateStatusAsync(message, new TwitterAdapter() { @Override public void onException(TwitterException e, int method) { LOGGER.warning("Exception updating Twitter status: " + e.toString()); } @Override public void updated(Status statuses) { LOGGER.info("Updated Twitter status: " + statuses.getText()); } }); } } }