Repository: WhisperSystems/BitHub Branch: master Commit: 82c9d21d2927 Files: 59 Total size: 125.9 KB Directory structure: gitextract_2c75r1ji/ ├── .gitignore ├── .travis.yml ├── Procfile ├── README.md ├── assembly.xml ├── config/ │ └── sample.yml ├── design/ │ ├── badge-small.xcf │ └── badge.xcf ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── org/ │ │ │ └── whispersystems/ │ │ │ └── bithub/ │ │ │ ├── BithubServerConfiguration.java │ │ │ ├── BithubService.java │ │ │ ├── auth/ │ │ │ │ └── GithubWebhookAuthenticator.java │ │ │ ├── client/ │ │ │ │ ├── CoinbaseClient.java │ │ │ │ ├── GithubClient.java │ │ │ │ └── TransferFailedException.java │ │ │ ├── config/ │ │ │ │ ├── BithubConfiguration.java │ │ │ │ ├── CoinbaseConfiguration.java │ │ │ │ ├── GithubConfiguration.java │ │ │ │ ├── OrganizationConfiguration.java │ │ │ │ ├── RepositoryConfiguration.java │ │ │ │ └── WebhookConfiguration.java │ │ │ ├── controllers/ │ │ │ │ ├── DashboardController.java │ │ │ │ ├── GithubController.java │ │ │ │ ├── StatusController.java │ │ │ │ └── UnauthorizedHookException.java │ │ │ ├── entities/ │ │ │ │ ├── Author.java │ │ │ │ ├── Commit.java │ │ │ │ ├── CommitComment.java │ │ │ │ ├── Payment.java │ │ │ │ ├── PushEvent.java │ │ │ │ ├── Repositories.java │ │ │ │ ├── Repository.java │ │ │ │ ├── Transaction.java │ │ │ │ └── Transactions.java │ │ │ ├── mappers/ │ │ │ │ ├── IOExceptionMapper.java │ │ │ │ └── UnauthorizedHookExceptionMapper.java │ │ │ ├── storage/ │ │ │ │ ├── CacheManager.java │ │ │ │ ├── CoinbaseTransactionParser.java │ │ │ │ └── CurrentPayment.java │ │ │ ├── util/ │ │ │ │ ├── AdvancedAtomicLong.java │ │ │ │ └── Badge.java │ │ │ └── views/ │ │ │ ├── DashboardView.java │ │ │ └── TransactionsView.java │ │ └── resources/ │ │ ├── banner.txt │ │ └── org/ │ │ └── whispersystems/ │ │ └── bithub/ │ │ └── views/ │ │ ├── dashboard.mustache │ │ └── recent_transactions.mustache │ └── test/ │ ├── java/ │ │ └── org/ │ │ └── whispersystems/ │ │ └── bithub/ │ │ └── tests/ │ │ ├── controllers/ │ │ │ ├── GithubControllerTest.java │ │ │ └── StatusControllerTest.java │ │ └── util/ │ │ └── JsonHelper.java │ └── resources/ │ └── payloads/ │ ├── invalid_origin.json │ ├── invalid_repo.json │ ├── multiple_commits_authors.json │ ├── no_opt_in_commit.json │ ├── non_master_push.json │ ├── opt_in_commit.json │ ├── opt_out_commit.json │ ├── transactions.json │ └── valid_commit.json └── system.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea *.iml target config/production.yml ================================================ FILE: .travis.yml ================================================ language: java ================================================ FILE: Procfile ================================================ web: java $JAVA_OPTS -Ddw.server.type=simple -Ddw.server.applicationContextPath=/ -Ddw.server.connector.type=http -Ddw.server.connector.port=$PORT -Ddw.github.user=$GITHUB_USER -Ddw.github.token=$GITHUB_TOKEN -Ddw.github.repositories_heroku="$GITHUB_REPOSITORIES" -Ddw.coinbase.apiKey=$COINBASE_API_KEY -Ddw.coinbase.apiSecret=$COINBASE_API_SECRET -Ddw.github.webhook.password=$GITHUB_WEBHOOK_PASSWORD -Ddw.organization.name="$ORGANIZATION_NAME" -Ddw.organization.donationUrl=$DONATION_URL -jar target/BitHub-0.1.jar server ================================================ FILE: README.md ================================================ BitHub ================= [![Build Status](https://travis-ci.org/WhisperSystems/BitHub.png?branch=master)](https://travis-ci.org/WhisperSystems/BitHub) BitHub is a service that will automatically pay a percentage of Bitcoin funds for every submission to a GitHub repository. More information can be found in our [announcement blog post](https://whispersystems.org/blog/bithub). Opting Out ---------- If you'd like to opt out of receiving a payment, simply include the string "FREEBIE" somewhere in your commit message, and you will not receive BTC for that commit. Building ------------- $ git clone https://github.com/WhisperSystems/BitHub.git $ cd BitHub $ mvn3 package Running ----------- 1. Create a GitHub account for your BitHub server. 1. Create a Coinbase account for your BitHub server. 1. Add the above credentials to `config/sample.yml` 1. Execute `$ java -jar target/BitHub-0.1.jar server config/yourconfig.yml` Deploying To Heroku ------------ ``` $ heroku create your_app_name $ heroku config:set GITHUB_USER=your_bithub_username $ heroku config:set GITHUB_TOKEN=your_bithub_authtoken $ heroku config:set GITHUB_WEBHOOK_PASSWORD=your_webhook_password $ heroku config:set GITHUB_REPOSITORIES="[{\"url\" : \"https://github.com/youraccount/yourrepo\"}, {\"url\" : \"https://github.com/youraccount/yourotherrepo\"}]" $ heroku config:set COINBASE_API_KEY=your_api_key $ heroku config:set ORGANIZATION_NAME=your_organization_name $ heroku config:set DONATION_URL=your_donation_url $ git remote add your_heroku_remote $ git push heroku master ``` Mailing list ------------ Have a question? Ask on our mailing list! whispersystems@lists.riseup.net https://lists.riseup.net/www/info/whispersystems Current BitHub Payment For Commit: ================= [![Current Price](https://bithub.herokuapp.com/v1/status/payment/commit)](https://whispersystems.org/blog/bithub/) ================================================ FILE: assembly.xml ================================================ bin false tar.gz ${project.basedir}/config /config * ${project.build.directory} / ${project.name}-${project.version}.jar ================================================ FILE: config/sample.yml ================================================ organization: name: # Your name (eg. Open Whisper Systems) donationUrl: # A Coinbase link where you can receive donations (eg. https://coinbase.com/checkouts/d29fd4c37ca442393e32fdcb95304701) github: user: # Your BitHub instance's GitHub username. token: # Your BitHub instance's GitHub auth token. webhook: password: # HTTP basic auth. The username defaults to "bithub". repositories: # A list of repository URLs to support payouts for. - url: # A repository's URL mode: # Either MONEYMONEY (default) or FREEBIE. # The former will pay out on every commit, unless # FREEBIE is specified in the message. The latter will # only pay out if MONEYMONEY is specified in the message. coinbase: apiKey: # Your Coinbase API key. apiSecret: # Your Coinbase API secret. ================================================ FILE: pom.xml ================================================ 4.0.0 3.0.0 org.whispersystems.bithub BitHub 0.1 0.7.0 io.dropwizard dropwizard-core ${dropwizard.version} io.dropwizard dropwizard-auth ${dropwizard.version} io.dropwizard dropwizard-jdbi ${dropwizard.version} io.dropwizard dropwizard-client ${dropwizard.version} io.dropwizard dropwizard-migrations ${dropwizard.version} io.dropwizard dropwizard-testing ${dropwizard.version} io.dropwizard dropwizard-metrics-graphite ${dropwizard.version} io.dropwizard dropwizard-views ${dropwizard.version} io.dropwizard dropwizard-views-mustache ${dropwizard.version} io.dropwizard dropwizard-servlets ${dropwizard.version} com.sun.jersey jersey-json 1.18.1 com.codahale.metrics metrics-graphite 3.0.2 com.sun.jersey.contribs jersey-multipart 1.18.1 commons-net commons-net 3.2 org.apache.commons commons-lang3 3.1 com.coinbase.api coinbase-java 1.9.1 com.fasterxml.jackson.core jackson-annotations 2.4.3 org.apache.maven.plugins maven-compiler-plugin 1.7 1.7 org.apache.maven.plugins maven-source-plugin 2.2.1 attach-sources jar org.apache.maven.plugins maven-jar-plugin 2.4 true org.apache.maven.plugins maven-shade-plugin 1.6 true *:* META-INF/*.SF META-INF/*.DSA META-INF/*.RSA package shade org.whispersystems.bithub.BithubService maven-assembly-plugin 2.4 assembly.xml make-assembly package single ================================================ FILE: src/main/java/org/whispersystems/bithub/BithubServerConfiguration.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub; import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.bithub.config.BithubConfiguration; import org.whispersystems.bithub.config.CoinbaseConfiguration; import org.whispersystems.bithub.config.GithubConfiguration; import org.whispersystems.bithub.config.OrganizationConfiguration; import javax.validation.Valid; import javax.validation.constraints.NotNull; import io.dropwizard.Configuration; public class BithubServerConfiguration extends Configuration { @Valid @NotNull @JsonProperty private GithubConfiguration github; @Valid @NotNull @JsonProperty private CoinbaseConfiguration coinbase; @JsonProperty @Valid private BithubConfiguration bithub = new BithubConfiguration(); @Valid @NotNull @JsonProperty private OrganizationConfiguration organization; public GithubConfiguration getGithubConfiguration() { return github; } public CoinbaseConfiguration getCoinbaseConfiguration() { return coinbase; } public BithubConfiguration getBithubConfiguration() { return bithub; } public OrganizationConfiguration getOrganizationConfiguration() { return organization; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/BithubService.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub; import org.eclipse.jetty.servlets.CrossOriginFilter; import org.whispersystems.bithub.auth.GithubWebhookAuthenticator; import org.whispersystems.bithub.client.CoinbaseClient; import org.whispersystems.bithub.client.GithubClient; import org.whispersystems.bithub.config.CoinbaseConfiguration; import org.whispersystems.bithub.config.RepositoryConfiguration; import org.whispersystems.bithub.controllers.DashboardController; import org.whispersystems.bithub.controllers.GithubController; import org.whispersystems.bithub.controllers.StatusController; import org.whispersystems.bithub.mappers.IOExceptionMapper; import org.whispersystems.bithub.mappers.UnauthorizedHookExceptionMapper; import org.whispersystems.bithub.storage.CacheManager; import javax.servlet.DispatcherType; import java.math.BigDecimal; import java.util.EnumSet; import java.util.List; import io.dropwizard.Application; import io.dropwizard.auth.basic.BasicAuthProvider; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import io.dropwizard.views.ViewBundle; /** * The main entry point for the service. * * @author Moxie Marlinspike */ public class BithubService extends Application { @Override public void initialize(Bootstrap bootstrap) { bootstrap.addBundle(new ViewBundle()); } @Override public void run(BithubServerConfiguration config, Environment environment) throws Exception { String githubUser = config.getGithubConfiguration().getUser(); String githubToken = config.getGithubConfiguration().getToken(); String githubWebhookUser = config.getGithubConfiguration().getWebhookConfiguration().getUsername(); String githubWebhookPwd = config.getGithubConfiguration().getWebhookConfiguration().getPassword(); List githubRepositories = config.getGithubConfiguration().getRepositories(); BigDecimal payoutRate = config.getBithubConfiguration().getPayoutRate(); String organizationName = config.getOrganizationConfiguration().getName(); String donationUrl = config.getOrganizationConfiguration().getDonationUrl().toExternalForm(); String coinbaseApiKey = config.getCoinbaseConfiguration().getApiKey(); String coinbaseApiSecret = config.getCoinbaseConfiguration().getApiSecret(); GithubClient githubClient = new GithubClient(githubUser, githubToken); CoinbaseClient coinbaseClient = new CoinbaseClient(coinbaseApiKey, coinbaseApiSecret); CacheManager cacheManager = new CacheManager(coinbaseClient, githubClient, githubRepositories, payoutRate); environment.servlets().addFilter("CORS", CrossOriginFilter.class) .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); environment.lifecycle().manage(cacheManager); environment.jersey().register(new GithubController(githubRepositories, githubClient, coinbaseClient, payoutRate)); environment.jersey().register(new StatusController(cacheManager, githubRepositories)); environment.jersey().register(new DashboardController(organizationName, donationUrl, cacheManager)); environment.jersey().register(new IOExceptionMapper()); environment.jersey().register(new UnauthorizedHookExceptionMapper()); environment.jersey().register(new BasicAuthProvider<>(new GithubWebhookAuthenticator(githubWebhookUser, githubWebhookPwd), GithubWebhookAuthenticator.REALM)); } public static void main(String[] args) throws Exception { new BithubService().run(args); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/auth/GithubWebhookAuthenticator.java ================================================ package org.whispersystems.bithub.auth; import com.google.common.base.Optional; import io.dropwizard.auth.Authenticator; import io.dropwizard.auth.basic.BasicCredentials; /** * Accepts only one fixed username/password combination. */ public class GithubWebhookAuthenticator implements Authenticator { /** * Represents a successful basic HTTP authentication. */ public static class Authentication { } public static final String REALM = "bithub"; private final BasicCredentials correctCredentials; public GithubWebhookAuthenticator(String username, String password) { this.correctCredentials = new BasicCredentials(username, password); } @Override public Optional authenticate(BasicCredentials clientCredentials) { if (correctCredentials.equals(clientCredentials)) { return Optional.of(new Authentication()); } else { return Optional.absent(); } } } ================================================ FILE: src/main/java/org/whispersystems/bithub/client/CoinbaseClient.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.client; import com.coinbase.api.Coinbase; import com.coinbase.api.CoinbaseBuilder; import com.coinbase.api.entity.Account; import com.coinbase.api.entity.Transaction; import com.coinbase.api.exception.CoinbaseException; import org.joda.money.CurrencyUnit; import org.joda.money.Money; import org.whispersystems.bithub.entities.Author; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; /** * Handles interaction with the Coinbase API. * * @author Moxie Marlinspike */ public class CoinbaseClient { private final Coinbase coinbase; public CoinbaseClient(String apiKey, String apiSecret) { this.coinbase = new CoinbaseBuilder().withApiKey(apiKey, apiSecret).build(); } public List getRecentTransactions() throws CoinbaseException, IOException { return coinbase.getTransactions().getTransactions(); } public BigDecimal getExchangeRate() throws IOException, CoinbaseException { return coinbase.getExchangeRates().get("btc_to_usd"); } public void sendPayment(Author author, BigDecimal amount, String url) throws TransferFailedException { try { String note = "Commit payment:\n__" + author.getUsername() + "__ " + url; Transaction transaction = new Transaction(); transaction.setTo(author.getEmail()); transaction.setAmount(Money.of(CurrencyUnit.of("BTC"), amount, RoundingMode.DOWN)); transaction.setNotes(note); Transaction response = coinbase.sendMoney(transaction); if (response.getStatus() != Transaction.Status.COMPLETE) { throw new TransferFailedException(); } } catch (CoinbaseException | IOException e) { throw new TransferFailedException(e); } } public BigDecimal getAccountBalance() throws IOException, CoinbaseException { List accounts = coinbase.getAccounts().getAccounts(); Account primary = null; for (Account account : accounts) { if (account.isPrimary()) { primary = account; break; } } if (primary != null) return coinbase.getBalance(primary.getId()).getAmount(); else return new BigDecimal(0.0); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/client/GithubClient.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.client; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.UniformInterfaceException; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; import com.sun.jersey.api.json.JSONConfiguration; import com.sun.jersey.core.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.bithub.entities.Commit; import org.whispersystems.bithub.entities.CommitComment; import org.whispersystems.bithub.entities.Repository; import javax.ws.rs.core.MediaType; /** * Handles interaction with the GitHub API. * * @author Moxie Marlinspike */ public class GithubClient { private static final String GITHUB_URL = "https://api.github.com/"; private static final String COMMENT_PATH = "/repos/%s/%s/commits/%s/comments"; private static final String COMMIT_PATH = "/repos/%s/%s/git/commits/%s"; private static final String REPOSITORY_PATH = "/repos/%s/%s"; private final Logger logger = LoggerFactory.getLogger(GithubClient.class); private final String authorizationHeader; private final Client client; public GithubClient(String user, String token) { this.authorizationHeader = getAuthorizationHeader(user, token); this.client = Client.create(getClientConfig()); } public String getCommitDescription(String commitUrl) { String[] commitUrlParts = commitUrl.split("/"); String owner = commitUrlParts[commitUrlParts.length - 4]; String repository = commitUrlParts[commitUrlParts.length - 3]; String commit = commitUrlParts[commitUrlParts.length - 1]; String path = String.format(COMMIT_PATH, owner, repository, commit); WebResource resource = client.resource(GITHUB_URL).path(path); Commit response = resource.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", authorizationHeader) .get(Commit.class); return response.getMessage(); } public Repository getRepository(String url) { String[] urlParts = url.split("/"); String owner = urlParts[urlParts.length - 2]; String name = urlParts[urlParts.length - 1]; String path = String.format(REPOSITORY_PATH, owner, name); WebResource resource = client.resource(GITHUB_URL).path(path); return resource.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE) .header("Authorization", authorizationHeader) .get(Repository.class); } public void addCommitComment(Repository repository, Commit commit, String comment) { try { String path = String.format(COMMENT_PATH, repository.getOwner().getName(), repository.getName(), commit.getSha()); WebResource resource = client.resource(GITHUB_URL).path(path); ClientResponse response = resource.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON) .header("Authorization", authorizationHeader) .entity(new CommitComment(comment)) .post(ClientResponse.class); if (response.getStatus() < 200 || response.getStatus() >=300) { logger.warn("Commit comment failed: " + response.getClientResponseStatus().getReasonPhrase()); } } catch (UniformInterfaceException | ClientHandlerException e) { logger.warn("Comment failed", e); } } private ClientConfig getClientConfig() { ClientConfig config = new DefaultClientConfig(); config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); return config; } private String getAuthorizationHeader(String user, String token) { return "Basic " + new String(Base64.encode(user + ":" + token)); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/client/TransferFailedException.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.client; public class TransferFailedException extends Exception { public TransferFailedException() { super(); } public TransferFailedException(Throwable e) { super(e); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/config/BithubConfiguration.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.config; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; import java.math.BigDecimal; public class BithubConfiguration { @JsonProperty @NotEmpty private String payout = "0.02"; public BigDecimal getPayoutRate() { return new BigDecimal(payout); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/config/CoinbaseConfiguration.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.config; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; public class CoinbaseConfiguration { @JsonProperty @NotEmpty private String apiKey; @JsonProperty @NotEmpty private String apiSecret; public String getApiKey() { return apiKey; } public String getApiSecret() { return apiSecret; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/config/GithubConfiguration.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.config; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.hibernate.validator.constraints.NotEmpty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.io.IOException; import java.util.LinkedList; import java.util.List; public class GithubConfiguration { private final Logger logger = LoggerFactory.getLogger(GithubConfiguration.class); @JsonProperty @NotEmpty private String user; @JsonProperty @NotEmpty private String token; @JsonProperty private List repositories; @JsonProperty private String repositories_heroku; @Valid @NotNull @JsonProperty private WebhookConfiguration webhook; public String getUser() { return user; } public String getToken() { return token; } public List getRepositories() { if (repositories != null) { return repositories; } if (repositories_heroku != null) { try { ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(repositories_heroku, new TypeReference>() {}); } catch (IOException e) { logger.warn("Error deserializing", e); } } return new LinkedList<>(); } public WebhookConfiguration getWebhookConfiguration() { return webhook; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/config/OrganizationConfiguration.java ================================================ package org.whispersystems.bithub.config; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.Valid; import java.net.URL; public class OrganizationConfiguration { @JsonProperty @NotEmpty private String name; @JsonProperty @Valid private URL donationUrl; public String getName() { return name; } public URL getDonationUrl() { return donationUrl; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/config/RepositoryConfiguration.java ================================================ package org.whispersystems.bithub.config; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; public class RepositoryConfiguration { @JsonProperty @NotEmpty private String url; @JsonProperty @NotEmpty private String mode = "MONEYMONEY"; public RepositoryConfiguration(String url, String mode) { this.url = url; this.mode = mode; } public RepositoryConfiguration(String url) { this.url = url; } public RepositoryConfiguration() {} public String getUrl() { return url; } public String getMode() { return mode; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/config/WebhookConfiguration.java ================================================ package org.whispersystems.bithub.config; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; public class WebhookConfiguration { @JsonProperty @NotEmpty private String username = "bithub"; @JsonProperty @NotEmpty private String password; public String getUsername() { return username; } public String getPassword() { return password; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/controllers/DashboardController.java ================================================ package org.whispersystems.bithub.controllers; import com.codahale.metrics.annotation.Timed; import org.whispersystems.bithub.storage.CacheManager; import org.whispersystems.bithub.views.DashboardView; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/") public class DashboardController { private final CacheManager cacheManager; private final String organizationName; private final String donationUrl; public DashboardController(String organizationName, String donationUrl, CacheManager cacheManager) { this.organizationName = organizationName; this.donationUrl = donationUrl; this.cacheManager = cacheManager; } @Timed @GET @Produces(MediaType.TEXT_HTML) public DashboardView getDashboard() { return new DashboardView(organizationName, donationUrl, cacheManager.getCurrentPaymentAmount(), cacheManager.getRepositories(), cacheManager.getRecentTransactions()); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/controllers/GithubController.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.controllers; import com.codahale.metrics.annotation.Timed; import com.coinbase.api.exception.CoinbaseException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.net.util.SubnetUtils; import org.apache.commons.net.util.SubnetUtils.SubnetInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.bithub.auth.GithubWebhookAuthenticator.Authentication; import org.whispersystems.bithub.client.CoinbaseClient; import org.whispersystems.bithub.client.GithubClient; import org.whispersystems.bithub.client.TransferFailedException; import org.whispersystems.bithub.config.RepositoryConfiguration; import org.whispersystems.bithub.entities.Commit; import org.whispersystems.bithub.entities.PushEvent; import org.whispersystems.bithub.entities.Repository; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import io.dropwizard.auth.Auth; /** * Handles incoming API calls from GitHub. These are currently only * PushEvent webhooks. * * @author Moxie Marlinspike */ @Path("/v1/github") public class GithubController { private static final String GITHUB_WEBOOK_CIDR = "192.30.252.0/22"; private static final String MASTER_REF = "refs/heads/master"; private final Logger logger = LoggerFactory.getLogger(GithubController.class); private final SubnetInfo trustedNetwork = new SubnetUtils(GITHUB_WEBOOK_CIDR).getInfo(); private final CoinbaseClient coinbaseClient; private final GithubClient githubClient; private final Map repositories; private final BigDecimal payoutRate; public GithubController(List repositories, GithubClient githubClient, CoinbaseClient coinbaseClient, BigDecimal payoutRate) { this.coinbaseClient = coinbaseClient; this.githubClient = githubClient; this.repositories = new HashMap<>(); this.payoutRate = payoutRate; for (RepositoryConfiguration repository : repositories) { this.repositories.put(repository.getUrl().toLowerCase(), repository.getMode().toUpperCase()); } } @Timed @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Path("/commits/") public void handleCommits(@Auth Authentication auth, @HeaderParam("X-Forwarded-For") String clientIp, @FormParam("payload") String eventString) throws IOException, UnauthorizedHookException, TransferFailedException, CoinbaseException { authenticate(clientIp); PushEvent event = getEventFromPayload(eventString); if (!repositories.containsKey(event.getRepository().getUrl().toLowerCase())) { throw new UnauthorizedHookException("Not a valid repository: " + event.getRepository().getUrl()); } if (!event.getRef().equals(MASTER_REF)) { logger.info("Not a push to master: " + event.getRef()); return; } Repository repository = event.getRepository(); String defaultMode = repositories.get(repository.getUrl().toLowerCase()); List commits = getQualifyingCommits(event, defaultMode); BigDecimal balance = coinbaseClient.getAccountBalance(); BigDecimal exchangeRate = coinbaseClient.getExchangeRate(); logger.info("Retrieved balance: " + balance.toPlainString()); sendPaymentsFor(repository, commits, balance, exchangeRate); } private void sendPaymentsFor(Repository repository, List commits, BigDecimal balance, BigDecimal exchangeRate) { for (Commit commit : commits) { try { BigDecimal payout = balance.multiply(payoutRate); if (isViablePaymentAmount(payout)) { coinbaseClient.sendPayment(commit.getAuthor(), payout, commit.getUrl()); } balance = balance.subtract(payout); githubClient.addCommitComment(repository, commit, getCommitCommentStringForPayment(payout, exchangeRate)); } catch (TransferFailedException e) { logger.warn("Transfer failed", e); } } } private PushEvent getEventFromPayload(String payload) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); PushEvent event = objectMapper.readValue(payload, PushEvent.class); ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); validator.validate(event); return event; } private List getQualifyingCommits(PushEvent event, String defaultMode) { List commits = new LinkedList<>(); Set emails = new HashSet<>(); for (Commit commit : event.getCommits()) { logger.info(commit.getUrl()); if (!emails.contains(commit.getAuthor().getEmail())) { logger.info("Unique author: "+ commit.getAuthor().getEmail()); if (isViableMessage(commit.getMessage(), defaultMode)) { logger.info("Not a merge commit or freebie..."); emails.add(commit.getAuthor().getEmail()); commits.add(commit); } } } return commits; } private boolean isViableMessage(String message, String defaultMode) { if (message == null || message.startsWith("Merge")) return false; return (!message.contains("FREEBIE") && defaultMode.equals("MONEYMONEY")) || (message.contains("MONEYMONEY") && defaultMode.equals("FREEBIE")); } private boolean isViablePaymentAmount(BigDecimal payment) { return payment.compareTo(new BigDecimal(0)) == 1; } private String getCommitCommentStringForPayment(BigDecimal payment, BigDecimal exchangeRate) { if (isViablePaymentAmount(payment)) { BigDecimal paymentUsd = payment.multiply(exchangeRate).setScale(2, RoundingMode.CEILING); return "Thanks! BitHub has sent payment of $" + paymentUsd.toPlainString() + "USD for this commit."; } else { return "Thanks! Unfortunately our BitHub balance is $0.00, so no payout can be made."; } } private void authenticate(String clientIp) throws UnauthorizedHookException { if (clientIp == null) { throw new UnauthorizedHookException("No X-Forwarded-For!"); } if (!trustedNetwork.isInRange(clientIp)) { throw new UnauthorizedHookException("Untrusted IP: " + clientIp); } } } ================================================ FILE: src/main/java/org/whispersystems/bithub/controllers/StatusController.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.controllers; import com.codahale.metrics.annotation.Timed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.bithub.config.RepositoryConfiguration; import org.whispersystems.bithub.entities.Repositories; import org.whispersystems.bithub.entities.Repository; import org.whispersystems.bithub.entities.Transaction; import org.whispersystems.bithub.entities.Transactions; import org.whispersystems.bithub.storage.CacheManager; import org.whispersystems.bithub.storage.CurrentPayment; import org.whispersystems.bithub.views.TransactionsView; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.LinkedList; import java.util.List; import io.dropwizard.jersey.caching.CacheControl; /** * Handles incoming API calls for BitHub instance status information. * * @author Moxie Marlinspike */ @Path("/v1/status") public class StatusController { private final Logger logger = LoggerFactory.getLogger(StatusController.class); private final List repositoryConfiguration; private final CacheManager coinbaseManager; public StatusController(CacheManager coinbaseManager, List repositoryConfiguration) throws IOException { this.coinbaseManager = coinbaseManager; this.repositoryConfiguration = repositoryConfiguration; } @Timed @GET @Path("/transactions") public Response getTransactions(@QueryParam("format") @DefaultValue("html") String format) throws IOException { List recentTransactions = coinbaseManager.getRecentTransactions(); switch (format) { case "html": return Response.ok(new TransactionsView(recentTransactions), MediaType.TEXT_HTML_TYPE).build(); case "json": default: return Response.ok(new Transactions(recentTransactions), MediaType.APPLICATION_JSON_TYPE).build(); } } @Timed @GET @Path("/repositories") @Produces(MediaType.APPLICATION_JSON) public Repositories getRepositories() { List repositories = new LinkedList<>(); for (RepositoryConfiguration configuration : repositoryConfiguration) { repositories.add(new Repository(configuration.getUrl())); } return new Repositories(repositories); } @Timed @GET @Path("/payment/commit") @CacheControl(noCache = true) public Response getCurrentCommitPrice(@QueryParam("format") @DefaultValue("png") String format) throws IOException { CurrentPayment currentPayment = coinbaseManager.getCurrentPaymentAmount(); switch (format) { case "json": return Response.ok(currentPayment.getEntity(), MediaType.APPLICATION_JSON_TYPE).build(); case "png_small": return Response.ok(currentPayment.getSmallBadge(), "image/png").build(); default: return Response.ok(currentPayment.getBadge(), "image/png").build(); } } } ================================================ FILE: src/main/java/org/whispersystems/bithub/controllers/UnauthorizedHookException.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.controllers; public class UnauthorizedHookException extends Throwable { public UnauthorizedHookException(String s) { super(s); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Author.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; @JsonIgnoreProperties(ignoreUnknown = true) public class Author { @JsonProperty private String name; @JsonProperty @NotEmpty private String email; @JsonProperty private String username; public String getName() { return name; } public String getEmail() { return email; } public String getUsername() { return username; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Commit.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotNull; @JsonIgnoreProperties(ignoreUnknown = true) public class Commit { @JsonProperty private String id; @JsonProperty private String message; @JsonProperty @NotNull private Author author; @JsonProperty private String url; @JsonProperty private boolean distinct; public String getSha() { return id; } public String getMessage() { return message; } public Author getAuthor() { return author; } public String getUrl() { return url; } public boolean isDistinct() { return distinct; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/CommitComment.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonProperty; public class CommitComment { @JsonProperty private String body; public CommitComment(String body) { this.body = body; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Payment.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonProperty; public class Payment { @JsonProperty private String payment; public Payment(String payment) { this.payment = payment; } public String getPayment() { return payment; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/PushEvent.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import javax.validation.constraints.NotNull; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class PushEvent { @JsonProperty private String head; @JsonProperty private String ref; @JsonProperty private int size; @JsonProperty @NotNull List commits; @JsonProperty @NotNull Repository repository; public Repository getRepository() { return repository; } public String getHead() { return head; } public String getRef() { return ref; } public int getSize() { return size; } public List getCommits() { return commits; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Repositories.java ================================================ package org.whispersystems.bithub.entities; import java.util.List; public class Repositories { public List repositories; public Repositories() {} public Repositories(List repositories) { this.repositories = repositories; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Repository.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.NotNull; @JsonIgnoreProperties(ignoreUnknown = true) public class Repository { @JsonProperty @NotEmpty private String url; @JsonProperty @NotNull private Author owner; @JsonProperty @NotEmpty private String name; @JsonProperty private String description; public Repository() {} public Repository(String url) { this.url = url; } public Author getOwner() { return owner; } public String getName() { return name; } public String getUrl() { return url; } public String getDescription() { return description; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Transaction.java ================================================ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonProperty; public class Transaction { @JsonProperty private String destination; @JsonProperty private String amount; @JsonProperty private String commitUrl; @JsonProperty private String commitSha; @JsonProperty private String timestamp; @JsonProperty private String description; public Transaction() {} public Transaction(String destination, String amount, String commitUrl, String commitSha, String timestamp, String description) { this.destination = destination; this.amount = amount; this.commitUrl = commitUrl; this.commitSha = commitSha; this.timestamp = timestamp; this.description = description; } public String getDestination() { return destination; } public String getAmount() { return amount; } public String getCommitUrl() { return commitUrl; } public String getCommitSha() { return commitSha; } public String getTimestamp() { return timestamp; } public String getDescription() { return description; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/entities/Transactions.java ================================================ package org.whispersystems.bithub.entities; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; public class Transactions { @JsonProperty private List transactions; public Transactions() {} public Transactions(List transactions) { this.transactions = transactions; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/mappers/IOExceptionMapper.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.mappers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; import java.io.IOException; @Provider public class IOExceptionMapper implements ExceptionMapper { private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class); @Override public Response toResponse(IOException e) { logger.warn("IOExceptionMapper", e); return Response.status(503).build(); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/mappers/UnauthorizedHookExceptionMapper.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.mappers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.bithub.controllers.UnauthorizedHookException; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider public class UnauthorizedHookExceptionMapper implements ExceptionMapper { private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class); @Override public Response toResponse(UnauthorizedHookException e) { logger.warn("IOExceptionMapper", e); return Response.status(401).build(); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/storage/CacheManager.java ================================================ package org.whispersystems.bithub.storage; import com.coinbase.api.exception.CoinbaseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.bithub.client.CoinbaseClient; import org.whispersystems.bithub.client.GithubClient; import org.whispersystems.bithub.config.RepositoryConfiguration; import org.whispersystems.bithub.entities.Payment; import org.whispersystems.bithub.entities.Repository; import org.whispersystems.bithub.entities.Transaction; import org.whispersystems.bithub.util.Badge; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import io.dropwizard.lifecycle.Managed; public class CacheManager implements Managed { private static final int UPDATE_FREQUENCY_MILLIS = 60 * 1000; private final Logger logger = LoggerFactory.getLogger(CacheManager.class); private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); private final CoinbaseClient coinbaseClient; private final GithubClient githubClient; private final BigDecimal payoutRate; private final List repositories; private AtomicReference cachedPaymentStatus; private AtomicReference> cachedTransactions; private AtomicReference> cachedRepositories; public CacheManager(CoinbaseClient coinbaseClient, GithubClient githubClient, List repositories, BigDecimal payoutRate) { this.coinbaseClient = coinbaseClient; this.githubClient = githubClient; this.payoutRate = payoutRate; this.repositories = repositories; } @Override public void start() throws Exception { this.cachedPaymentStatus = new AtomicReference<>(createCurrentPaymentForBalance(coinbaseClient)); this.cachedTransactions = new AtomicReference<>(createRecentTransactions(coinbaseClient)); this.cachedRepositories = new AtomicReference<>(createRepositories(githubClient, repositories)); initializeUpdates(coinbaseClient, githubClient, repositories); } @Override public void stop() throws Exception { this.executor.shutdownNow(); } public List getRecentTransactions() { return cachedTransactions.get(); } public CurrentPayment getCurrentPaymentAmount() { return cachedPaymentStatus.get(); } public List getRepositories() { return cachedRepositories.get(); } public void initializeUpdates(final CoinbaseClient coinbaseClient, final GithubClient githubClient, final List repoConfigs) { executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { logger.warn("Running cache update..."); try { CurrentPayment currentPayment = createCurrentPaymentForBalance(coinbaseClient); List transactions = createRecentTransactions (coinbaseClient); List repositories = createRepositories(githubClient, repoConfigs); cachedPaymentStatus.set(currentPayment); cachedTransactions.set(transactions); cachedRepositories.set(repositories); } catch (IOException | CoinbaseException e) { logger.warn("Failed to update badge", e); } } }, UPDATE_FREQUENCY_MILLIS, UPDATE_FREQUENCY_MILLIS, TimeUnit.MILLISECONDS); } private List createRepositories(GithubClient githubClient, List configured) { List repositoryList = new LinkedList<>(); for (RepositoryConfiguration repository : configured) { repositoryList.add(githubClient.getRepository(repository.getUrl())); } return repositoryList; } private CurrentPayment createCurrentPaymentForBalance(CoinbaseClient coinbaseClient) throws IOException, CoinbaseException { BigDecimal currentBalance = coinbaseClient.getAccountBalance(); BigDecimal paymentBtc = currentBalance.multiply(payoutRate); BigDecimal exchangeRate = coinbaseClient.getExchangeRate(); BigDecimal paymentUsd = paymentBtc.multiply(exchangeRate); paymentUsd = paymentUsd.setScale(2, RoundingMode.CEILING); return new CurrentPayment(Badge.createFor(paymentUsd.toPlainString()), Badge.createSmallFor(paymentUsd.toPlainString()), new Payment(paymentUsd.toPlainString())); } private List createRecentTransactions(CoinbaseClient coinbaseClient) throws IOException, CoinbaseException { List recentTransactions = coinbaseClient.getRecentTransactions(); BigDecimal exchangeRate = coinbaseClient.getExchangeRate(); List transactions = new LinkedList<>(); for (com.coinbase.api.entity.Transaction coinbaseTransaction : recentTransactions) { try { if (isSentTransaction(coinbaseTransaction)) { CoinbaseTransactionParser parser = new CoinbaseTransactionParser(coinbaseTransaction); String url = parser.parseUrlFromMessage(); String sha = parser.parseShaFromUrl(url); String description = githubClient.getCommitDescription(url); transactions.add(new Transaction(parser.parseDestinationFromMessage(), parser.parseAmountInDollars(exchangeRate), url, sha, parser.parseTimestamp(), description)); if (transactions.size() >= 10) break; } } catch (ParseException e) { logger.warn("Parse", e); } } return transactions; } private boolean isSentTransaction(com.coinbase.api.entity.Transaction coinbaseTransaction) { BigDecimal amount = coinbaseTransaction.getAmount().getAmount(); return amount.compareTo(new BigDecimal(0.0)) < 0; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/storage/CoinbaseTransactionParser.java ================================================ package org.whispersystems.bithub.storage; import com.coinbase.api.entity.Transaction; import org.apache.commons.lang3.StringEscapeUtils; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; public class CoinbaseTransactionParser { private final Transaction coinbaseTransaction; public CoinbaseTransactionParser(Transaction coinbaseTransaction) { this.coinbaseTransaction = coinbaseTransaction; } public String parseAmountInDollars(BigDecimal exchangeRate) { return coinbaseTransaction.getAmount().getAmount().abs() .multiply(exchangeRate) .setScale(2, RoundingMode.CEILING) .toPlainString(); } public String parseTimestamp() throws ParseException { DateTime timestamp = coinbaseTransaction.getCreatedAt(); DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZ"); return fmt.print(timestamp); } public String parseDestinationFromMessage() { String message = StringEscapeUtils.unescapeHtml4(coinbaseTransaction.getNotes()); int startToken = message.indexOf("__"); if (startToken == -1) { return "Unknown"; } int endToken = message.indexOf("__", startToken + 1); if (endToken == -1) { return "Unknown"; } return message.substring(startToken+2, endToken); } public String parseUrlFromMessage() throws ParseException { String message = StringEscapeUtils.unescapeHtml4(coinbaseTransaction.getNotes()); int urlIndex = message.indexOf("https://"); return message.substring(urlIndex).trim(); } public String parseShaFromUrl(String url) throws ParseException { if (url == null) { throw new ParseException("No url", 0); } String[] parts = url.split("/"); String fullHash = parts[parts.length-1]; if (fullHash.length() < 8) { throw new ParseException("Not long enough", 0); } return fullHash.substring(0, 8); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/storage/CurrentPayment.java ================================================ package org.whispersystems.bithub.storage; import org.whispersystems.bithub.entities.Payment; public class CurrentPayment { private final byte[] badge; private final byte[] smallBadge; private final Payment entity; protected CurrentPayment(byte[] badge, byte[] smallBadge, Payment entity) { this.badge = badge; this.smallBadge = smallBadge; this.entity = entity; } public byte[] getBadge() { return badge; } public byte[] getSmallBadge() { return smallBadge; } public Payment getEntity() { return entity; } } ================================================ FILE: src/main/java/org/whispersystems/bithub/util/AdvancedAtomicLong.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.util; import java.util.concurrent.atomic.AtomicLong; public class AdvancedAtomicLong extends AtomicLong { public AdvancedAtomicLong(long initial) { super(initial); } public boolean setIfGreater(long compare, long update) { while(true) { long current = get(); if (compare > current) { if (compareAndSet(current, update)) { return true; } } else { return false; } } } } ================================================ FILE: src/main/java/org/whispersystems/bithub/util/Badge.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.util; import com.google.common.io.Resources; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class Badge { public static byte[] createFor(String price) throws IOException { byte[] badgeBackground = Resources.toByteArray(Resources.getResource("assets/badge.png")); BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(badgeBackground)); Graphics2D graphics = bufferedImage.createGraphics(); graphics.setFont(new Font("OpenSans", Font.PLAIN, 34)); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); graphics.drawString(price + " USD", 86, 45); graphics.dispose(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "png", baos); return baos.toByteArray(); } public static byte[] createSmallFor(String price) throws IOException { byte[] badgeBackground = Resources.toByteArray(Resources.getResource("assets/badge-small.png")); BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(badgeBackground)); Graphics2D graphics = bufferedImage.createGraphics(); graphics.setFont(new Font("OpenSans", Font.PLAIN, 9)); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); graphics.drawString(price + " USD", 22, 14); graphics.dispose(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, "png", baos); return baos.toByteArray(); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/views/DashboardView.java ================================================ package org.whispersystems.bithub.views; import org.whispersystems.bithub.entities.Repository; import org.whispersystems.bithub.entities.Transaction; import org.whispersystems.bithub.storage.CurrentPayment; import java.util.List; import io.dropwizard.views.View; public class DashboardView extends View { private final String organizationName; private final String donationUrl; private final CurrentPayment currentPayment; private final List repositories; private final List transactions; public DashboardView(String organizationName, String donationUrl, CurrentPayment currentPayment, List repositories, List transactions) { super("dashboard.mustache"); this.organizationName = organizationName; this.donationUrl = donationUrl; this.currentPayment = currentPayment; this.repositories = repositories; this.transactions = transactions; } public String getPayment() { return currentPayment.getEntity().getPayment(); } public String getOrganizationName() { return organizationName; } public String getDonationUrl() { return donationUrl; } public List getRepositories() { return repositories; } public List getTransactions() { return transactions; } public String getRepositoriesCount() { return String.valueOf(repositories.size()); } } ================================================ FILE: src/main/java/org/whispersystems/bithub/views/TransactionsView.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.views; import org.whispersystems.bithub.entities.Transaction; import java.util.List; import io.dropwizard.views.View; /** * A rendered HTML view of recent BitHub transactions. * * @author Moxie Marlinspike */ public class TransactionsView extends View { private final List transactions; public TransactionsView(List transactions) { super("recent_transactions.mustache"); this.transactions = transactions; } public List getTransactions() { return transactions; } } ================================================ FILE: src/main/resources/banner.txt ================================================ 888888b. d8b 888 888 888 888 888 "88b Y8P 888 888 888 888 888 .88P 888 888 888 888 8888888K. 888 888888 8888888888 888 888 88888b. 888 "Y88b 888 888 888 888 888 888 888 "88b 888 888 888 888 888 888 888 888 888 888 888 d88P 888 Y88b. 888 888 Y88b 888 888 d88P 8888888P" 888 "Y888 888 888 "Y88888 88888P" ================================================ FILE: src/main/resources/org/whispersystems/bithub/views/dashboard.mustache ================================================ BitHub :: Dashboard

BitHub

{{organizationName}}

Donate BTC today

{{payment}}
0
{{repositoriesCount}}

Recent payments

{{#transactions}} {{/transactions}}
USD Author Description Commit
${{amount}} USD {{destination}}
{{description}}
{{commitSha}}

Repositories

{{#repositories}}

{{description}}

{{/repositories}}
================================================ FILE: src/main/resources/org/whispersystems/bithub/views/recent_transactions.mustache ================================================ ================================================ FILE: src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java ================================================ /** * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package org.whispersystems.bithub.tests.controllers; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.core.util.MultivaluedMapImpl; import org.apache.commons.codec.binary.Base64; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.whispersystems.bithub.auth.GithubWebhookAuthenticator; import org.whispersystems.bithub.client.CoinbaseClient; import org.whispersystems.bithub.client.GithubClient; import org.whispersystems.bithub.client.TransferFailedException; import org.whispersystems.bithub.config.RepositoryConfiguration; import org.whispersystems.bithub.controllers.GithubController; import org.whispersystems.bithub.entities.Author; import org.whispersystems.bithub.mappers.UnauthorizedHookExceptionMapper; import javax.ws.rs.core.MediaType; import java.io.InputStream; import java.math.BigDecimal; import java.util.LinkedList; import java.util.List; import java.util.Scanner; import io.dropwizard.auth.basic.BasicAuthProvider; import io.dropwizard.testing.junit.ResourceTestRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class GithubControllerTest { private static final BigDecimal BALANCE = new BigDecimal(10.01); private static final BigDecimal EXCHANGE_RATE = new BigDecimal(1.0); private final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class); private final GithubClient githubClient = mock(GithubClient.class); // HTTP Basic Authentication data private final String authUsername = "TestUser"; private final String authPassword = "TestPassword"; private final String authRealm = GithubWebhookAuthenticator.REALM; private final String authString = "Basic " + Base64.encodeBase64String((authUsername + ":" + authPassword).getBytes()); private final String invalidUserAuthString = "Basic " + Base64.encodeBase64(("wrong:" + authPassword).getBytes()); private final String invalidPasswordAuthString = "Basic " + Base64.encodeBase64((authUsername + ":wrong").getBytes()); private final List repositories = new LinkedList() {{ add(new RepositoryConfiguration("https://github.com/moxie0/test")); add(new RepositoryConfiguration("https://github.com/moxie0/optin", "FREEBIE")); }}; @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(new UnauthorizedHookExceptionMapper()) .addProvider(new BasicAuthProvider<>(new GithubWebhookAuthenticator(authUsername, authPassword), authRealm)) .addResource(new GithubController(repositories, githubClient, coinbaseClient, new BigDecimal(0.02))) .build(); @Before public void setup() throws Exception, TransferFailedException { when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE); when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE); } protected String payload(String path) { InputStream is = this.getClass().getResourceAsStream(path); Scanner s = new Scanner(is).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } @Test public void testInvalidRepository() throws Exception { String payloadValue = payload("/payloads/invalid_repo.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); assertThat(response.getStatus()).isEqualTo(401); } @Test public void testInvalidOrigin() throws Exception { String payloadValue = payload("/payloads/invalid_origin.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.242.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); assertThat(response.getStatus()).isEqualTo(401); } @Test public void testMissingAuth() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/valid_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); assertThat(response.getStatus()).isEqualTo(401); } @Test public void testInvalidAuthUser() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/valid_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", invalidUserAuthString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); assertThat(response.getStatus()).isEqualTo(401); } @Test public void testInvalidAuthPassword() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/valid_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", invalidPasswordAuthString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); assertThat(response.getStatus()).isEqualTo(401); } @Test public void testOptOutCommit() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/opt_out_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); verify(coinbaseClient, never()).sendPayment(any(Author.class), any(BigDecimal.class), anyString()); } @Test public void testValidCommit() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/valid_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); verify(coinbaseClient).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); } @Test public void testNonMaster() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/non_master_push.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); verify(coinbaseClient, never()).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); } @Test public void testValidMultipleCommitsMultipleAuthors() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/multiple_commits_authors.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.subtract(BALANCE.multiply(new BigDecimal(0.02))) .multiply(new BigDecimal(0.02))), anyString()); } @Test public void testOptInCommit() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/opt_in_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); verify(coinbaseClient).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); } @Test public void testNoOptInCommit() throws Exception, TransferFailedException { String payloadValue = payload("/payloads/no_opt_in_commit.json"); MultivaluedMapImpl post = new MultivaluedMapImpl(); post.add("payload", payloadValue); ClientResponse response = resources.client().resource("/v1/github/commits/") .header("X-Forwarded-For", "192.30.252.1") .header("Authorization", authString) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); verify(coinbaseClient, never()).sendPayment(any(Author.class), any(BigDecimal.class), anyString()); } } ================================================ FILE: src/test/java/org/whispersystems/bithub/tests/controllers/StatusControllerTest.java ================================================ package org.whispersystems.bithub.tests.controllers; import com.coinbase.api.ObjectMapperProvider; import com.coinbase.api.entity.TransactionsResponse; import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.jersey.api.client.ClientResponse; import org.junit.ClassRule; import org.junit.Test; import org.whispersystems.bithub.client.CoinbaseClient; import org.whispersystems.bithub.client.GithubClient; import org.whispersystems.bithub.config.RepositoryConfiguration; import org.whispersystems.bithub.controllers.StatusController; import org.whispersystems.bithub.storage.CacheManager; import javax.ws.rs.core.MediaType; import java.math.BigDecimal; import java.util.LinkedList; import io.dropwizard.testing.junit.ResourceTestRule; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class StatusControllerTest { private static final BigDecimal PAYOUT_RATE = new BigDecimal(0.02 ); private static final BigDecimal BALANCE = new BigDecimal(10.01); private static final BigDecimal EXCHANGE_RATE = new BigDecimal(1.0 ); private static final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class); private static final GithubClient githubClient = mock(GithubClient.class ); @ClassRule public static ResourceTestRule resources; static { try { ObjectMapper objectMapper = ObjectMapperProvider.createDefaultMapper(); TransactionsResponse transactionsResponse = objectMapper.readValue(StatusControllerTest.class.getResourceAsStream("/payloads/transactions.json"), TransactionsResponse.class); when(coinbaseClient.getRecentTransactions()).thenReturn(transactionsResponse.getTransactions()); when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE); when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE); CacheManager coinbaseManager = new CacheManager(coinbaseClient, githubClient, new LinkedList(), PAYOUT_RATE); coinbaseManager.start(); resources = ResourceTestRule.builder() .addResource(new StatusController(coinbaseManager, null)) .build(); } catch (Exception e) { throw new AssertionError(e); } } // @Before // public void setup() throws Exception { // when(coinbaseClient.getRecentTransactions()).thenReturn(fromJson(jsonFixture("payloads/transactions.json"), RecentTransactionsResponse.class).getTransactions()); // when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE); // when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE); // // } // @Test // public void testTransactionsHtml() throws Exception { // ClientResponse response = resources.client().resource("/v1/status/transactions/") // .get(ClientResponse.class); // // assertThat(response.getStatus()).isEqualTo(200); // assertThat(response.getType()).isEqualTo(MediaType.TEXT_HTML_TYPE); // } @Test public void testTransactionsJson() throws Exception { ClientResponse response = resources.client().resource("/v1/status/transactions/?format=json").accept(MediaType.APPLICATION_JSON_TYPE) .get(ClientResponse.class); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE); } } ================================================ FILE: src/test/java/org/whispersystems/bithub/tests/util/JsonHelper.java ================================================ package org.whispersystems.bithub.tests.util; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import static io.dropwizard.testing.FixtureHelpers.fixture; public class JsonHelper { private static final ObjectMapper objectMapper = new ObjectMapper(); public static String asJson(Object object) throws JsonProcessingException { return objectMapper.writeValueAsString(object); } public static T fromJson(String value, Class clazz) throws IOException { return objectMapper.readValue(value, clazz); } public static String jsonFixture(String filename) throws IOException { return objectMapper.writeValueAsString(objectMapper.readValue(fixture(filename), JsonNode.class)); } } ================================================ FILE: src/test/resources/payloads/invalid_origin.json ================================================ { "after": "100e9859651b35a3505cc278e9a98a076f79940b", "before": "6626766348ab245bdb3351989f753bd6e792524a", "commits": [ { "added": [], "author": { "email": "info@whispersystems.org", "name": "WhisperBTC", "username": "WhisperBTC" }, "committer": { "email": "info@whispersystems.org", "name": "WhisperBTC", "username": "WhisperBTC" }, "distinct": true, "id": "fd7daeb1de6d72220b1313a7f1112d43885013aa", "message": "Update foo", "modified": [ "foo" ], "removed": [], "timestamp": "2013-12-14T11:27:00-08:00", "url": "https://github.com/moxie0/tempt/commit/fd7daeb1de6d72220b1313a7f1112d43885013aa" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "100e9859651b35a3505cc278e9a98a076f79940b", "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", "modified": [ "foo" ], "removed": [], "timestamp": "2013-12-14T11:27:28-08:00", "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" } ], "compare": "https://github.com/moxie0/tempt/compare/6626766348ab...100e9859651b", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "100e9859651b35a3505cc278e9a98a076f79940b", "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", "modified": [ "foo" ], "removed": [], "timestamp": "2013-12-14T11:27:28-08:00", "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/master", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387049248, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/test", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/invalid_repo.json ================================================ { "after": "100e9859651b35a3505cc278e9a98a076f79940b", "before": "6626766348ab245bdb3351989f753bd6e792524a", "commits": [ { "added": [], "author": { "email": "info@whispersystems.org", "name": "WhisperBTC", "username": "WhisperBTC" }, "committer": { "email": "info@whispersystems.org", "name": "WhisperBTC", "username": "WhisperBTC" }, "distinct": true, "id": "fd7daeb1de6d72220b1313a7f1112d43885013aa", "message": "Update foo", "modified": [ "foo" ], "removed": [], "timestamp": "2013-12-14T11:27:00-08:00", "url": "https://github.com/moxie0/tempt/commit/fd7daeb1de6d72220b1313a7f1112d43885013aa" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "100e9859651b35a3505cc278e9a98a076f79940b", "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", "modified": [ "foo" ], "removed": [], "timestamp": "2013-12-14T11:27:28-08:00", "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" } ], "compare": "https://github.com/moxie0/tempt/compare/6626766348ab...100e9859651b", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "100e9859651b35a3505cc278e9a98a076f79940b", "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", "modified": [ "foo" ], "removed": [], "timestamp": "2013-12-14T11:27:28-08:00", "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/master", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387049248, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/tempt", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/multiple_commits_authors.json ================================================ { "after": "1481a2de7b2a7d02428ad93446ab166be7793fbb", "before": "17c497ccc7cca9c2f735aa07e9e3813060ce9a6a", "commits": [ { "added": [], "author": { "email": "otherauthor@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "committer": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "distinct": true, "id": "c441029cf673f84c8b7db52d0a5944ee5c52ff89", "message": "Test", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-02-22T13:50:07-08:00", "url": "https://github.com/octokitty/testing/commit/c441029cf673f84c8b7db52d0a5944ee5c52ff89" }, { "added": [], "author": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "committer": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "distinct": true, "id": "36c5f2243ed24de58284a96f2a643bed8c028658", "message": "This is me testing the windows client.", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-02-22T14:07:13-08:00", "url": "https://github.com/octokitty/testing/commit/36c5f2243ed24de58284a96f2a643bed8c028658" }, { "added": [ "words/madame-bovary.txt" ], "author": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "committer": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "distinct": true, "id": "1481a2de7b2a7d02428ad93446ab166be7793fbb", "message": "Rename madame-bovary.txt to words/madame-bovary.txt", "modified": [], "removed": [ "madame-bovary.txt" ], "timestamp": "2013-03-12T08:14:29-07:00", "url": "https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb" } ], "compare": "https://github.com/octokitty/testing/compare/17c497ccc7cc...1481a2de7b2a", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [ "words/madame-bovary.txt" ], "author": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "committer": { "email": "lolwut@noway.biz", "name": "Garen Torikian", "username": "octokitty" }, "distinct": true, "id": "1481a2de7b2a7d02428ad93446ab166be7793fbb", "message": "Rename madame-bovary.txt to words/madame-bovary.txt", "modified": [], "removed": [ "madame-bovary.txt" ], "timestamp": "2013-03-12T08:14:29-07:00", "url": "https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb" }, "pusher": { "email": "lolwut@noway.biz", "name": "Garen Torikian" }, "ref": "refs/heads/master", "repository": { "created_at": 1332977768, "description": "", "fork": false, "forks": 0, "has_downloads": true, "has_issues": true, "has_wiki": true, "homepage": "", "id": 3860742, "language": "Ruby", "master_branch": "master", "name": "testing", "open_issues": 2, "owner": { "email": "lolwut@noway.biz", "name": "octokitty" }, "private": false, "pushed_at": 1363295520, "size": 2156, "stargazers": 1, "url": "https://github.com/moxie0/test", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/no_opt_in_commit.json ================================================ { "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", "commits": [ { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", "message": "Update path", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-12-14T11:42:28-08:00", "url": "https://github.com/moxie0/optin/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" } ], "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/optin", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/master", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387050173, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/optin", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/non_master_push.json ================================================ { "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", "commits": [ { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", "message": "Update path", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-12-14T11:42:28-08:00", "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" } ], "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/lilia_doing_something", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387050173, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/test", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/opt_in_commit.json ================================================ { "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", "commits": [ { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", "message": "Update path MONEYMONEY", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-12-14T11:42:28-08:00", "url": "https://github.com/moxie0/optin/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" } ], "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/optin", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/master", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387050173, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/optin", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/opt_out_commit.json ================================================ { "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", "commits": [ { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", "message": "Update path FREEBIE", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-12-14T11:42:28-08:00", "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt FREEBIE", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" } ], "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/master", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387050173, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/test", "watchers": 1 } } ================================================ FILE: src/test/resources/payloads/transactions.json ================================================ { "current_user": { "id": "5011f33df8182b142400000e", "email": "user2@example.com", "name": "User Two" }, "balance": { "amount": "50.00000000", "currency": "BTC" }, "total_count": 2, "num_pages": 1, "current_page": 1, "transactions": [ { "transaction": { "id": "5018f833f8182b129c00002f", "created_at": "2012-08-01T02:34:43-07:00", "amount": { "amount": "-1.10000000", "currency": "BTC" }, "request": true, "status": "pending", "sender": { "id": "5011f33df8182b142400000e", "name": "User Two", "email": "user2@example.com" }, "recipient": { "id": "5011f33df8182b142400000a", "name": "User One", "email": "user1@example.com" }, "notes": "Commit payment:__moxie0__ https://github.com/WhisperSystems/BitHub/commit/88edf54e5b57c80ac05093a9be90965fd41291c2" } }, { "transaction": { "id": "5018f833f8182b129c00002e", "created_at": "2012-08-01T02:36:43-07:00", "hsh": "9d6a7d1112c3db9de5315b421a5153d71413f5f752aff75bf504b77df4e646a3", "amount": { "amount": "-1.00000000", "currency": "BTC" }, "request": false, "status": "complete", "sender": { "id": "5011f33df8182b142400000e", "name": "User Two", "email": "user2@example.com" }, "recipient_address": "37muSN5ZrukVTvyVh3mT5Zc5ew9L9CBare", "notes": "Commit payment:__moxie0__ https://github.com/WhisperSystems/BitHub/commit/88edf54e5b57c80ac05093a9be90965fd41291c2" } } ] } ================================================ FILE: src/test/resources/payloads/valid_commit.json ================================================ { "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", "commits": [ { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", "message": "Update path", "modified": [ "README.md" ], "removed": [], "timestamp": "2013-12-14T11:42:28-08:00", "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" }, { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" } ], "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", "created": false, "deleted": false, "forced": false, "head_commit": { "added": [], "author": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "committer": { "email": "moxie@thoughtcrime.org", "name": "Moxie Marlinspike", "username": "moxie0" }, "distinct": true, "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", "message": "Merge branch 'master' of github.com:moxie0/tempt", "modified": [], "removed": [], "timestamp": "2013-12-14T11:42:44-08:00", "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" }, "pusher": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "ref": "refs/heads/master", "repository": { "created_at": 1386866024, "description": "test", "fork": false, "forks": 1, "has_downloads": true, "has_issues": true, "has_wiki": true, "id": 15141344, "master_branch": "master", "name": "tempt", "open_issues": 0, "owner": { "email": "moxie@thoughtcrime.org", "name": "moxie0" }, "private": false, "pushed_at": 1387050173, "size": 216, "stargazers": 1, "url": "https://github.com/moxie0/test", "watchers": 1 } } ================================================ FILE: system.properties ================================================ java.runtime.version=1.7