├── .gitignore ├── .travis.yml ├── Procfile ├── README.md ├── assembly.xml ├── config └── sample.yml ├── design ├── badge-small.png ├── badge-small.xcf ├── badge.png └── 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 │ │ ├── assets │ │ ├── badge-small.png │ │ └── badge.png │ │ ├── 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 /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | config/production.yml 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | 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 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BitHub 2 | ================= 3 | 4 | [](https://travis-ci.org/WhisperSystems/BitHub) 5 | 6 | BitHub is a service that will automatically pay a percentage of Bitcoin funds for every submission to a GitHub repository. 7 | 8 | More information can be found in our [announcement blog post](https://whispersystems.org/blog/bithub). 9 | 10 | Opting Out 11 | ---------- 12 | 13 | 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. 14 | 15 | 16 | Building 17 | ------------- 18 | 19 | $ git clone https://github.com/WhisperSystems/BitHub.git 20 | $ cd BitHub 21 | $ mvn3 package 22 | 23 | Running 24 | ----------- 25 | 26 | 1. Create a GitHub account for your BitHub server. 27 | 1. Create a Coinbase account for your BitHub server. 28 | 1. Add the above credentials to `config/sample.yml` 29 | 1. Execute `$ java -jar target/BitHub-0.1.jar server config/yourconfig.yml` 30 | 31 | Deploying To Heroku 32 | ------------ 33 | 34 | ``` 35 | $ heroku create your_app_name 36 | $ heroku config:set GITHUB_USER=your_bithub_username 37 | $ heroku config:set GITHUB_TOKEN=your_bithub_authtoken 38 | $ heroku config:set GITHUB_WEBHOOK_PASSWORD=your_webhook_password 39 | $ heroku config:set GITHUB_REPOSITORIES="[{\"url\" : \"https://github.com/youraccount/yourrepo\"}, {\"url\" : \"https://github.com/youraccount/yourotherrepo\"}]" 40 | $ heroku config:set COINBASE_API_KEY=your_api_key 41 | $ heroku config:set ORGANIZATION_NAME=your_organization_name 42 | $ heroku config:set DONATION_URL=your_donation_url 43 | $ git remote add your_heroku_remote 44 | $ git push heroku master 45 | ``` 46 | 47 | Mailing list 48 | ------------ 49 | 50 | Have a question? Ask on our mailing list! 51 | 52 | whispersystems@lists.riseup.net 53 | 54 | https://lists.riseup.net/www/info/whispersystems 55 | 56 | Current BitHub Payment For Commit: 57 | ================= 58 | [](https://whispersystems.org/blog/bithub/) 59 | 60 | -------------------------------------------------------------------------------- /assembly.xml: -------------------------------------------------------------------------------- 1 | 4 | bin 5 | false 6 | 7 | tar.gz 8 | 9 | 10 | 11 | ${project.basedir}/config 12 | /config 13 | 14 | * 15 | 16 | 17 | 18 | ${project.build.directory} 19 | / 20 | 21 | ${project.name}-${project.version}.jar 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /config/sample.yml: -------------------------------------------------------------------------------- 1 | organization: 2 | name: # Your name (eg. Open Whisper Systems) 3 | donationUrl: # A Coinbase link where you can receive donations (eg. https://coinbase.com/checkouts/d29fd4c37ca442393e32fdcb95304701) 4 | 5 | github: 6 | user: # Your BitHub instance's GitHub username. 7 | token: # Your BitHub instance's GitHub auth token. 8 | 9 | webhook: 10 | password: # HTTP basic auth. The username defaults to "bithub". 11 | 12 | repositories: # A list of repository URLs to support payouts for. 13 | - url: # A repository's URL 14 | mode: # Either MONEYMONEY (default) or FREEBIE. 15 | # The former will pay out on every commit, unless 16 | # FREEBIE is specified in the message. The latter will 17 | # only pay out if MONEYMONEY is specified in the message. 18 | 19 | coinbase: 20 | apiKey: # Your Coinbase API key. 21 | apiSecret: # Your Coinbase API secret. 22 | -------------------------------------------------------------------------------- /design/badge-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalapp/BitHub/82c9d21d292772e35ac40c8269001add2aa3e884/design/badge-small.png -------------------------------------------------------------------------------- /design/badge-small.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalapp/BitHub/82c9d21d292772e35ac40c8269001add2aa3e884/design/badge-small.xcf -------------------------------------------------------------------------------- /design/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalapp/BitHub/82c9d21d292772e35ac40c8269001add2aa3e884/design/badge.png -------------------------------------------------------------------------------- /design/badge.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalapp/BitHub/82c9d21d292772e35ac40c8269001add2aa3e884/design/badge.xcf -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 3.0.0 8 | 9 | 10 | org.whispersystems.bithub 11 | BitHub 12 | 0.1 13 | 14 | 15 | 0.7.0 16 | 17 | 18 | 19 | 20 | io.dropwizard 21 | dropwizard-core 22 | ${dropwizard.version} 23 | 24 | 25 | io.dropwizard 26 | dropwizard-auth 27 | ${dropwizard.version} 28 | 29 | 30 | io.dropwizard 31 | dropwizard-jdbi 32 | ${dropwizard.version} 33 | 34 | 35 | io.dropwizard 36 | dropwizard-client 37 | ${dropwizard.version} 38 | 39 | 40 | io.dropwizard 41 | dropwizard-migrations 42 | ${dropwizard.version} 43 | 44 | 45 | io.dropwizard 46 | dropwizard-testing 47 | ${dropwizard.version} 48 | 49 | 50 | io.dropwizard 51 | dropwizard-metrics-graphite 52 | ${dropwizard.version} 53 | 54 | 55 | io.dropwizard 56 | dropwizard-views 57 | ${dropwizard.version} 58 | 59 | 60 | io.dropwizard 61 | dropwizard-views-mustache 62 | ${dropwizard.version} 63 | 64 | 65 | io.dropwizard 66 | dropwizard-servlets 67 | ${dropwizard.version} 68 | 69 | 70 | 71 | com.sun.jersey 72 | jersey-json 73 | 1.18.1 74 | 75 | 76 | com.codahale.metrics 77 | metrics-graphite 78 | 3.0.2 79 | 80 | 81 | 82 | com.sun.jersey.contribs 83 | jersey-multipart 84 | 1.18.1 85 | 86 | 87 | commons-net 88 | commons-net 89 | 3.2 90 | 91 | 92 | org.apache.commons 93 | commons-lang3 94 | 3.1 95 | 96 | 97 | 98 | com.coinbase.api 99 | coinbase-java 100 | 1.9.1 101 | 102 | 103 | com.fasterxml.jackson.core 104 | jackson-annotations 105 | 2.4.3 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-compiler-plugin 115 | 116 | 1.7 117 | 1.7 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-source-plugin 123 | 2.2.1 124 | 125 | 126 | attach-sources 127 | 128 | jar 129 | 130 | 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-jar-plugin 136 | 2.4 137 | 138 | 139 | 140 | true 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-shade-plugin 148 | 1.6 149 | 150 | true 151 | 152 | 153 | *:* 154 | 155 | META-INF/*.SF 156 | META-INF/*.DSA 157 | META-INF/*.RSA 158 | 159 | 160 | 161 | 162 | 163 | 164 | package 165 | 166 | shade 167 | 168 | 169 | 170 | 171 | 172 | org.whispersystems.bithub.BithubService 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | maven-assembly-plugin 182 | 2.4 183 | 184 | 185 | assembly.xml 186 | 187 | 188 | 189 | 190 | make-assembly 191 | package 192 | 193 | single 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/BithubServerConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub; 19 | 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import org.whispersystems.bithub.config.BithubConfiguration; 22 | import org.whispersystems.bithub.config.CoinbaseConfiguration; 23 | import org.whispersystems.bithub.config.GithubConfiguration; 24 | import org.whispersystems.bithub.config.OrganizationConfiguration; 25 | 26 | import javax.validation.Valid; 27 | import javax.validation.constraints.NotNull; 28 | 29 | import io.dropwizard.Configuration; 30 | 31 | public class BithubServerConfiguration extends Configuration { 32 | 33 | @Valid 34 | @NotNull 35 | @JsonProperty 36 | private GithubConfiguration github; 37 | 38 | @Valid 39 | @NotNull 40 | @JsonProperty 41 | private CoinbaseConfiguration coinbase; 42 | 43 | @JsonProperty 44 | @Valid 45 | private BithubConfiguration bithub = new BithubConfiguration(); 46 | 47 | @Valid 48 | @NotNull 49 | @JsonProperty 50 | private OrganizationConfiguration organization; 51 | 52 | 53 | public GithubConfiguration getGithubConfiguration() { 54 | return github; 55 | } 56 | 57 | public CoinbaseConfiguration getCoinbaseConfiguration() { 58 | return coinbase; 59 | } 60 | 61 | public BithubConfiguration getBithubConfiguration() { 62 | return bithub; 63 | } 64 | 65 | public OrganizationConfiguration getOrganizationConfiguration() { 66 | return organization; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/BithubService.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub; 19 | 20 | import org.eclipse.jetty.servlets.CrossOriginFilter; 21 | import org.whispersystems.bithub.auth.GithubWebhookAuthenticator; 22 | import org.whispersystems.bithub.client.CoinbaseClient; 23 | import org.whispersystems.bithub.client.GithubClient; 24 | import org.whispersystems.bithub.config.CoinbaseConfiguration; 25 | import org.whispersystems.bithub.config.RepositoryConfiguration; 26 | import org.whispersystems.bithub.controllers.DashboardController; 27 | import org.whispersystems.bithub.controllers.GithubController; 28 | import org.whispersystems.bithub.controllers.StatusController; 29 | import org.whispersystems.bithub.mappers.IOExceptionMapper; 30 | import org.whispersystems.bithub.mappers.UnauthorizedHookExceptionMapper; 31 | import org.whispersystems.bithub.storage.CacheManager; 32 | 33 | import javax.servlet.DispatcherType; 34 | import java.math.BigDecimal; 35 | import java.util.EnumSet; 36 | import java.util.List; 37 | 38 | import io.dropwizard.Application; 39 | import io.dropwizard.auth.basic.BasicAuthProvider; 40 | import io.dropwizard.setup.Bootstrap; 41 | import io.dropwizard.setup.Environment; 42 | import io.dropwizard.views.ViewBundle; 43 | 44 | /** 45 | * The main entry point for the service. 46 | * 47 | * @author Moxie Marlinspike 48 | */ 49 | public class BithubService extends Application { 50 | 51 | @Override 52 | public void initialize(Bootstrap bootstrap) { 53 | bootstrap.addBundle(new ViewBundle()); 54 | } 55 | 56 | @Override 57 | public void run(BithubServerConfiguration config, Environment environment) 58 | throws Exception 59 | { 60 | String githubUser = config.getGithubConfiguration().getUser(); 61 | String githubToken = config.getGithubConfiguration().getToken(); 62 | String githubWebhookUser = config.getGithubConfiguration().getWebhookConfiguration().getUsername(); 63 | String githubWebhookPwd = config.getGithubConfiguration().getWebhookConfiguration().getPassword(); 64 | List githubRepositories = config.getGithubConfiguration().getRepositories(); 65 | BigDecimal payoutRate = config.getBithubConfiguration().getPayoutRate(); 66 | String organizationName = config.getOrganizationConfiguration().getName(); 67 | String donationUrl = config.getOrganizationConfiguration().getDonationUrl().toExternalForm(); 68 | String coinbaseApiKey = config.getCoinbaseConfiguration().getApiKey(); 69 | String coinbaseApiSecret = config.getCoinbaseConfiguration().getApiSecret(); 70 | 71 | GithubClient githubClient = new GithubClient(githubUser, githubToken); 72 | CoinbaseClient coinbaseClient = new CoinbaseClient(coinbaseApiKey, coinbaseApiSecret); 73 | CacheManager cacheManager = new CacheManager(coinbaseClient, githubClient, githubRepositories, payoutRate); 74 | 75 | environment.servlets().addFilter("CORS", CrossOriginFilter.class) 76 | .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); 77 | 78 | environment.lifecycle().manage(cacheManager); 79 | 80 | environment.jersey().register(new GithubController(githubRepositories, githubClient, coinbaseClient, payoutRate)); 81 | environment.jersey().register(new StatusController(cacheManager, githubRepositories)); 82 | environment.jersey().register(new DashboardController(organizationName, donationUrl, cacheManager)); 83 | 84 | environment.jersey().register(new IOExceptionMapper()); 85 | environment.jersey().register(new UnauthorizedHookExceptionMapper()); 86 | environment.jersey().register(new BasicAuthProvider<>(new GithubWebhookAuthenticator(githubWebhookUser, githubWebhookPwd), 87 | GithubWebhookAuthenticator.REALM)); 88 | } 89 | 90 | public static void main(String[] args) throws Exception { 91 | new BithubService().run(args); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/auth/GithubWebhookAuthenticator.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.auth; 2 | 3 | import com.google.common.base.Optional; 4 | 5 | import io.dropwizard.auth.Authenticator; 6 | import io.dropwizard.auth.basic.BasicCredentials; 7 | 8 | /** 9 | * Accepts only one fixed username/password combination. 10 | */ 11 | public class GithubWebhookAuthenticator implements Authenticator { 12 | 13 | /** 14 | * Represents a successful basic HTTP authentication. 15 | */ 16 | public static class Authentication { 17 | } 18 | 19 | public static final String REALM = "bithub"; 20 | 21 | private final BasicCredentials correctCredentials; 22 | 23 | public GithubWebhookAuthenticator(String username, String password) { 24 | this.correctCredentials = new BasicCredentials(username, password); 25 | } 26 | 27 | @Override 28 | public Optional authenticate(BasicCredentials clientCredentials) { 29 | if (correctCredentials.equals(clientCredentials)) { 30 | return Optional.of(new Authentication()); 31 | } else { 32 | return Optional.absent(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/client/CoinbaseClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.client; 19 | 20 | import com.coinbase.api.Coinbase; 21 | import com.coinbase.api.CoinbaseBuilder; 22 | import com.coinbase.api.entity.Account; 23 | import com.coinbase.api.entity.Transaction; 24 | import com.coinbase.api.exception.CoinbaseException; 25 | import org.joda.money.CurrencyUnit; 26 | import org.joda.money.Money; 27 | import org.whispersystems.bithub.entities.Author; 28 | 29 | import java.io.IOException; 30 | import java.math.BigDecimal; 31 | import java.math.RoundingMode; 32 | import java.util.List; 33 | 34 | /** 35 | * Handles interaction with the Coinbase API. 36 | * 37 | * @author Moxie Marlinspike 38 | */ 39 | public class CoinbaseClient { 40 | 41 | private final Coinbase coinbase; 42 | 43 | public CoinbaseClient(String apiKey, String apiSecret) { 44 | this.coinbase = new CoinbaseBuilder().withApiKey(apiKey, apiSecret).build(); 45 | } 46 | 47 | public List getRecentTransactions() 48 | throws CoinbaseException, IOException 49 | { 50 | return coinbase.getTransactions().getTransactions(); 51 | } 52 | 53 | public BigDecimal getExchangeRate() throws IOException, CoinbaseException { 54 | return coinbase.getExchangeRates().get("btc_to_usd"); 55 | } 56 | 57 | public void sendPayment(Author author, BigDecimal amount, String url) 58 | throws TransferFailedException 59 | { 60 | try { 61 | String note = "Commit payment:\n__" + author.getUsername() + "__ " + url; 62 | 63 | Transaction transaction = new Transaction(); 64 | transaction.setTo(author.getEmail()); 65 | transaction.setAmount(Money.of(CurrencyUnit.of("BTC"), amount, RoundingMode.DOWN)); 66 | transaction.setNotes(note); 67 | 68 | Transaction response = coinbase.sendMoney(transaction); 69 | 70 | if (response.getStatus() != Transaction.Status.COMPLETE) { 71 | throw new TransferFailedException(); 72 | } 73 | } catch (CoinbaseException | IOException e) { 74 | throw new TransferFailedException(e); 75 | } 76 | } 77 | 78 | public BigDecimal getAccountBalance() throws IOException, CoinbaseException { 79 | List accounts = coinbase.getAccounts().getAccounts(); 80 | Account primary = null; 81 | 82 | for (Account account : accounts) { 83 | if (account.isPrimary()) { 84 | primary = account; 85 | break; 86 | } 87 | } 88 | 89 | if (primary != null) return coinbase.getBalance(primary.getId()).getAmount(); 90 | else return new BigDecimal(0.0); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/client/GithubClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.client; 19 | 20 | import com.sun.jersey.api.client.Client; 21 | import com.sun.jersey.api.client.ClientHandlerException; 22 | import com.sun.jersey.api.client.ClientResponse; 23 | import com.sun.jersey.api.client.UniformInterfaceException; 24 | import com.sun.jersey.api.client.WebResource; 25 | import com.sun.jersey.api.client.config.ClientConfig; 26 | import com.sun.jersey.api.client.config.DefaultClientConfig; 27 | import com.sun.jersey.api.json.JSONConfiguration; 28 | import com.sun.jersey.core.util.Base64; 29 | 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | import org.whispersystems.bithub.entities.Commit; 33 | import org.whispersystems.bithub.entities.CommitComment; 34 | import org.whispersystems.bithub.entities.Repository; 35 | 36 | import javax.ws.rs.core.MediaType; 37 | 38 | /** 39 | * Handles interaction with the GitHub API. 40 | * 41 | * @author Moxie Marlinspike 42 | */ 43 | public class GithubClient { 44 | 45 | private static final String GITHUB_URL = "https://api.github.com/"; 46 | private static final String COMMENT_PATH = "/repos/%s/%s/commits/%s/comments"; 47 | private static final String COMMIT_PATH = "/repos/%s/%s/git/commits/%s"; 48 | private static final String REPOSITORY_PATH = "/repos/%s/%s"; 49 | 50 | private final Logger logger = LoggerFactory.getLogger(GithubClient.class); 51 | 52 | private final String authorizationHeader; 53 | private final Client client; 54 | 55 | public GithubClient(String user, String token) { 56 | this.authorizationHeader = getAuthorizationHeader(user, token); 57 | this.client = Client.create(getClientConfig()); 58 | } 59 | 60 | public String getCommitDescription(String commitUrl) { 61 | String[] commitUrlParts = commitUrl.split("/"); 62 | String owner = commitUrlParts[commitUrlParts.length - 4]; 63 | String repository = commitUrlParts[commitUrlParts.length - 3]; 64 | String commit = commitUrlParts[commitUrlParts.length - 1]; 65 | 66 | String path = String.format(COMMIT_PATH, owner, repository, commit); 67 | WebResource resource = client.resource(GITHUB_URL).path(path); 68 | Commit response = resource.type(MediaType.APPLICATION_JSON_TYPE) 69 | .accept(MediaType.APPLICATION_JSON_TYPE) 70 | .header("Authorization", authorizationHeader) 71 | .get(Commit.class); 72 | 73 | return response.getMessage(); 74 | } 75 | 76 | public Repository getRepository(String url) { 77 | String[] urlParts = url.split("/"); 78 | String owner = urlParts[urlParts.length - 2]; 79 | String name = urlParts[urlParts.length - 1]; 80 | 81 | String path = String.format(REPOSITORY_PATH, owner, name); 82 | WebResource resource = client.resource(GITHUB_URL).path(path); 83 | 84 | return resource.type(MediaType.APPLICATION_JSON_TYPE) 85 | .accept(MediaType.APPLICATION_JSON_TYPE) 86 | .header("Authorization", authorizationHeader) 87 | .get(Repository.class); 88 | 89 | } 90 | 91 | public void addCommitComment(Repository repository, Commit commit, String comment) { 92 | try { 93 | String path = String.format(COMMENT_PATH, repository.getOwner().getName(), 94 | repository.getName(), commit.getSha()); 95 | 96 | WebResource resource = client.resource(GITHUB_URL).path(path); 97 | ClientResponse response = resource.type(MediaType.APPLICATION_JSON_TYPE) 98 | .accept(MediaType.APPLICATION_JSON) 99 | .header("Authorization", authorizationHeader) 100 | .entity(new CommitComment(comment)) 101 | .post(ClientResponse.class); 102 | 103 | if (response.getStatus() < 200 || response.getStatus() >=300) { 104 | logger.warn("Commit comment failed: " + response.getClientResponseStatus().getReasonPhrase()); 105 | } 106 | 107 | } catch (UniformInterfaceException | ClientHandlerException e) { 108 | logger.warn("Comment failed", e); 109 | } 110 | } 111 | 112 | private ClientConfig getClientConfig() { 113 | ClientConfig config = new DefaultClientConfig(); 114 | config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); 115 | 116 | return config; 117 | } 118 | 119 | private String getAuthorizationHeader(String user, String token) { 120 | return "Basic " + new String(Base64.encode(user + ":" + token)); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/client/TransferFailedException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.client; 19 | 20 | public class TransferFailedException extends Exception { 21 | 22 | public TransferFailedException() { 23 | super(); 24 | } 25 | 26 | public TransferFailedException(Throwable e) { 27 | super(e); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/config/BithubConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.config; 19 | 20 | 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | 23 | import org.hibernate.validator.constraints.NotEmpty; 24 | 25 | import java.math.BigDecimal; 26 | 27 | public class BithubConfiguration { 28 | 29 | @JsonProperty 30 | @NotEmpty 31 | private String payout = "0.02"; 32 | 33 | public BigDecimal getPayoutRate() { 34 | return new BigDecimal(payout); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/config/CoinbaseConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.config; 19 | 20 | 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import org.hibernate.validator.constraints.NotEmpty; 23 | 24 | public class CoinbaseConfiguration { 25 | 26 | @JsonProperty 27 | @NotEmpty 28 | private String apiKey; 29 | 30 | @JsonProperty 31 | @NotEmpty 32 | private String apiSecret; 33 | 34 | public String getApiKey() { 35 | return apiKey; 36 | } 37 | 38 | public String getApiSecret() { 39 | return apiSecret; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/config/GithubConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.config; 19 | 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | import com.fasterxml.jackson.core.type.TypeReference; 22 | import com.fasterxml.jackson.databind.ObjectMapper; 23 | import org.hibernate.validator.constraints.NotEmpty; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import javax.validation.Valid; 28 | import javax.validation.constraints.NotNull; 29 | import java.io.IOException; 30 | import java.util.LinkedList; 31 | import java.util.List; 32 | 33 | public class GithubConfiguration { 34 | 35 | private final Logger logger = LoggerFactory.getLogger(GithubConfiguration.class); 36 | 37 | @JsonProperty 38 | @NotEmpty 39 | private String user; 40 | 41 | @JsonProperty 42 | @NotEmpty 43 | private String token; 44 | 45 | @JsonProperty 46 | private List repositories; 47 | 48 | @JsonProperty 49 | private String repositories_heroku; 50 | 51 | @Valid 52 | @NotNull 53 | @JsonProperty 54 | private WebhookConfiguration webhook; 55 | 56 | public String getUser() { 57 | return user; 58 | } 59 | 60 | public String getToken() { 61 | return token; 62 | } 63 | 64 | public List getRepositories() { 65 | if (repositories != null) { 66 | return repositories; 67 | } 68 | 69 | if (repositories_heroku != null) { 70 | try { 71 | ObjectMapper mapper = new ObjectMapper(); 72 | return mapper.readValue(repositories_heroku, new TypeReference>() {}); 73 | } catch (IOException e) { 74 | logger.warn("Error deserializing", e); 75 | } 76 | } 77 | 78 | return new LinkedList<>(); 79 | } 80 | 81 | public WebhookConfiguration getWebhookConfiguration() { 82 | return webhook; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/config/OrganizationConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.hibernate.validator.constraints.NotEmpty; 5 | 6 | import javax.validation.Valid; 7 | import java.net.URL; 8 | 9 | public class OrganizationConfiguration { 10 | 11 | @JsonProperty 12 | @NotEmpty 13 | private String name; 14 | 15 | @JsonProperty 16 | @Valid 17 | private URL donationUrl; 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public URL getDonationUrl() { 24 | return donationUrl; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/config/RepositoryConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.hibernate.validator.constraints.NotEmpty; 5 | 6 | public class RepositoryConfiguration { 7 | 8 | @JsonProperty 9 | @NotEmpty 10 | private String url; 11 | 12 | @JsonProperty 13 | @NotEmpty 14 | private String mode = "MONEYMONEY"; 15 | 16 | public RepositoryConfiguration(String url, String mode) { 17 | this.url = url; 18 | this.mode = mode; 19 | } 20 | 21 | public RepositoryConfiguration(String url) { 22 | this.url = url; 23 | } 24 | 25 | public RepositoryConfiguration() {} 26 | 27 | public String getUrl() { 28 | return url; 29 | } 30 | 31 | public String getMode() { 32 | return mode; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/config/WebhookConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.hibernate.validator.constraints.NotEmpty; 5 | 6 | public class WebhookConfiguration { 7 | 8 | @JsonProperty 9 | @NotEmpty 10 | private String username = "bithub"; 11 | 12 | @JsonProperty 13 | @NotEmpty 14 | private String password; 15 | 16 | public String getUsername() { return username; } 17 | 18 | public String getPassword() { return password; } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/controllers/DashboardController.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.controllers; 2 | 3 | import com.codahale.metrics.annotation.Timed; 4 | import org.whispersystems.bithub.storage.CacheManager; 5 | import org.whispersystems.bithub.views.DashboardView; 6 | 7 | import javax.ws.rs.GET; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.Produces; 10 | import javax.ws.rs.core.MediaType; 11 | 12 | @Path("/") 13 | public class DashboardController { 14 | 15 | private final CacheManager cacheManager; 16 | private final String organizationName; 17 | private final String donationUrl; 18 | 19 | 20 | 21 | public DashboardController(String organizationName, String donationUrl, 22 | CacheManager cacheManager) 23 | { 24 | this.organizationName = organizationName; 25 | this.donationUrl = donationUrl; 26 | this.cacheManager = cacheManager; 27 | } 28 | 29 | @Timed 30 | @GET 31 | @Produces(MediaType.TEXT_HTML) 32 | public DashboardView getDashboard() { 33 | return new DashboardView(organizationName, donationUrl, 34 | cacheManager.getCurrentPaymentAmount(), 35 | cacheManager.getRepositories(), 36 | cacheManager.getRecentTransactions()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/controllers/GithubController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.controllers; 19 | 20 | import com.codahale.metrics.annotation.Timed; 21 | import com.coinbase.api.exception.CoinbaseException; 22 | import com.fasterxml.jackson.databind.ObjectMapper; 23 | import org.apache.commons.net.util.SubnetUtils; 24 | import org.apache.commons.net.util.SubnetUtils.SubnetInfo; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.whispersystems.bithub.auth.GithubWebhookAuthenticator.Authentication; 28 | import org.whispersystems.bithub.client.CoinbaseClient; 29 | import org.whispersystems.bithub.client.GithubClient; 30 | import org.whispersystems.bithub.client.TransferFailedException; 31 | import org.whispersystems.bithub.config.RepositoryConfiguration; 32 | import org.whispersystems.bithub.entities.Commit; 33 | import org.whispersystems.bithub.entities.PushEvent; 34 | import org.whispersystems.bithub.entities.Repository; 35 | 36 | import javax.validation.Validation; 37 | import javax.validation.Validator; 38 | import javax.validation.ValidatorFactory; 39 | import javax.ws.rs.Consumes; 40 | import javax.ws.rs.FormParam; 41 | import javax.ws.rs.HeaderParam; 42 | import javax.ws.rs.POST; 43 | import javax.ws.rs.Path; 44 | import javax.ws.rs.core.MediaType; 45 | import java.io.IOException; 46 | import java.math.BigDecimal; 47 | import java.math.RoundingMode; 48 | import java.util.HashMap; 49 | import java.util.HashSet; 50 | import java.util.LinkedList; 51 | import java.util.List; 52 | import java.util.Map; 53 | import java.util.Set; 54 | 55 | import io.dropwizard.auth.Auth; 56 | 57 | /** 58 | * Handles incoming API calls from GitHub. These are currently only 59 | * PushEvent webhooks. 60 | * 61 | * @author Moxie Marlinspike 62 | */ 63 | @Path("/v1/github") 64 | public class GithubController { 65 | 66 | private static final String GITHUB_WEBOOK_CIDR = "192.30.252.0/22"; 67 | private static final String MASTER_REF = "refs/heads/master"; 68 | 69 | private final Logger logger = LoggerFactory.getLogger(GithubController.class); 70 | private final SubnetInfo trustedNetwork = new SubnetUtils(GITHUB_WEBOOK_CIDR).getInfo(); 71 | 72 | private final CoinbaseClient coinbaseClient; 73 | private final GithubClient githubClient; 74 | private final Map repositories; 75 | private final BigDecimal payoutRate; 76 | 77 | public GithubController(List repositories, 78 | GithubClient githubClient, 79 | CoinbaseClient coinbaseClient, 80 | BigDecimal payoutRate) 81 | { 82 | this.coinbaseClient = coinbaseClient; 83 | this.githubClient = githubClient; 84 | this.repositories = new HashMap<>(); 85 | this.payoutRate = payoutRate; 86 | 87 | for (RepositoryConfiguration repository : repositories) { 88 | this.repositories.put(repository.getUrl().toLowerCase(), 89 | repository.getMode().toUpperCase()); 90 | } 91 | } 92 | 93 | @Timed 94 | @POST 95 | @Consumes(MediaType.APPLICATION_FORM_URLENCODED) 96 | @Path("/commits/") 97 | public void handleCommits(@Auth Authentication auth, 98 | @HeaderParam("X-Forwarded-For") String clientIp, 99 | @FormParam("payload") String eventString) 100 | throws IOException, UnauthorizedHookException, TransferFailedException, CoinbaseException 101 | { 102 | authenticate(clientIp); 103 | PushEvent event = getEventFromPayload(eventString); 104 | 105 | if (!repositories.containsKey(event.getRepository().getUrl().toLowerCase())) { 106 | throw new UnauthorizedHookException("Not a valid repository: " + 107 | event.getRepository().getUrl()); 108 | } 109 | 110 | if (!event.getRef().equals(MASTER_REF)) { 111 | logger.info("Not a push to master: " + event.getRef()); 112 | return; 113 | } 114 | 115 | Repository repository = event.getRepository(); 116 | String defaultMode = repositories.get(repository.getUrl().toLowerCase()); 117 | List commits = getQualifyingCommits(event, defaultMode); 118 | BigDecimal balance = coinbaseClient.getAccountBalance(); 119 | BigDecimal exchangeRate = coinbaseClient.getExchangeRate(); 120 | 121 | logger.info("Retrieved balance: " + balance.toPlainString()); 122 | 123 | sendPaymentsFor(repository, commits, balance, exchangeRate); 124 | } 125 | 126 | 127 | private void sendPaymentsFor(Repository repository, List commits, 128 | BigDecimal balance, BigDecimal exchangeRate) 129 | { 130 | for (Commit commit : commits) { 131 | try { 132 | BigDecimal payout = balance.multiply(payoutRate); 133 | 134 | if (isViablePaymentAmount(payout)) { 135 | coinbaseClient.sendPayment(commit.getAuthor(), payout, commit.getUrl()); 136 | } 137 | 138 | balance = balance.subtract(payout); 139 | 140 | githubClient.addCommitComment(repository, commit, 141 | getCommitCommentStringForPayment(payout, exchangeRate)); 142 | } catch (TransferFailedException e) { 143 | logger.warn("Transfer failed", e); 144 | } 145 | } 146 | } 147 | 148 | private PushEvent getEventFromPayload(String payload) throws IOException { 149 | ObjectMapper objectMapper = new ObjectMapper(); 150 | PushEvent event = objectMapper.readValue(payload, PushEvent.class); 151 | ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); 152 | Validator validator = factory.getValidator(); 153 | 154 | validator.validate(event); 155 | return event; 156 | } 157 | 158 | private List getQualifyingCommits(PushEvent event, String defaultMode) { 159 | List commits = new LinkedList<>(); 160 | Set emails = new HashSet<>(); 161 | 162 | for (Commit commit : event.getCommits()) { 163 | logger.info(commit.getUrl()); 164 | if (!emails.contains(commit.getAuthor().getEmail())) { 165 | logger.info("Unique author: "+ commit.getAuthor().getEmail()); 166 | if (isViableMessage(commit.getMessage(), defaultMode)) { 167 | logger.info("Not a merge commit or freebie..."); 168 | 169 | emails.add(commit.getAuthor().getEmail()); 170 | commits.add(commit); 171 | } 172 | } 173 | } 174 | 175 | return commits; 176 | } 177 | 178 | private boolean isViableMessage(String message, String defaultMode) { 179 | if (message == null || message.startsWith("Merge")) 180 | return false; 181 | 182 | return (!message.contains("FREEBIE") && defaultMode.equals("MONEYMONEY")) || 183 | (message.contains("MONEYMONEY") && defaultMode.equals("FREEBIE")); 184 | } 185 | 186 | private boolean isViablePaymentAmount(BigDecimal payment) { 187 | return payment.compareTo(new BigDecimal(0)) == 1; 188 | } 189 | 190 | private String getCommitCommentStringForPayment(BigDecimal payment, BigDecimal exchangeRate) { 191 | if (isViablePaymentAmount(payment)) { 192 | BigDecimal paymentUsd = payment.multiply(exchangeRate).setScale(2, RoundingMode.CEILING); 193 | return "Thanks! BitHub has sent payment of $" + paymentUsd.toPlainString() + "USD for this commit."; 194 | } else { 195 | return "Thanks! Unfortunately our BitHub balance is $0.00, so no payout can be made."; 196 | } 197 | } 198 | 199 | private void authenticate(String clientIp) throws UnauthorizedHookException { 200 | if (clientIp == null) { 201 | throw new UnauthorizedHookException("No X-Forwarded-For!"); 202 | } 203 | 204 | if (!trustedNetwork.isInRange(clientIp)) { 205 | throw new UnauthorizedHookException("Untrusted IP: " + clientIp); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/controllers/StatusController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.controllers; 19 | 20 | import com.codahale.metrics.annotation.Timed; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.whispersystems.bithub.config.RepositoryConfiguration; 24 | import org.whispersystems.bithub.entities.Repositories; 25 | import org.whispersystems.bithub.entities.Repository; 26 | import org.whispersystems.bithub.entities.Transaction; 27 | import org.whispersystems.bithub.entities.Transactions; 28 | import org.whispersystems.bithub.storage.CacheManager; 29 | import org.whispersystems.bithub.storage.CurrentPayment; 30 | import org.whispersystems.bithub.views.TransactionsView; 31 | 32 | import javax.ws.rs.DefaultValue; 33 | import javax.ws.rs.GET; 34 | import javax.ws.rs.Path; 35 | import javax.ws.rs.Produces; 36 | import javax.ws.rs.QueryParam; 37 | import javax.ws.rs.core.MediaType; 38 | import javax.ws.rs.core.Response; 39 | import java.io.IOException; 40 | import java.util.LinkedList; 41 | import java.util.List; 42 | 43 | import io.dropwizard.jersey.caching.CacheControl; 44 | 45 | /** 46 | * Handles incoming API calls for BitHub instance status information. 47 | * 48 | * @author Moxie Marlinspike 49 | */ 50 | @Path("/v1/status") 51 | public class StatusController { 52 | 53 | private final Logger logger = LoggerFactory.getLogger(StatusController.class); 54 | 55 | private final List repositoryConfiguration; 56 | private final CacheManager coinbaseManager; 57 | 58 | public StatusController(CacheManager coinbaseManager, 59 | List repositoryConfiguration) 60 | throws IOException 61 | { 62 | this.coinbaseManager = coinbaseManager; 63 | this.repositoryConfiguration = repositoryConfiguration; 64 | } 65 | 66 | @Timed 67 | @GET 68 | @Path("/transactions") 69 | public Response getTransactions(@QueryParam("format") @DefaultValue("html") String format) 70 | throws IOException 71 | { 72 | List recentTransactions = coinbaseManager.getRecentTransactions(); 73 | 74 | switch (format) { 75 | case "html": return Response.ok(new TransactionsView(recentTransactions), MediaType.TEXT_HTML_TYPE).build(); 76 | case "json": 77 | default: return Response.ok(new Transactions(recentTransactions), MediaType.APPLICATION_JSON_TYPE).build(); 78 | } 79 | } 80 | 81 | @Timed 82 | @GET 83 | @Path("/repositories") 84 | @Produces(MediaType.APPLICATION_JSON) 85 | public Repositories getRepositories() { 86 | List repositories = new LinkedList<>(); 87 | 88 | for (RepositoryConfiguration configuration : repositoryConfiguration) { 89 | repositories.add(new Repository(configuration.getUrl())); 90 | } 91 | 92 | return new Repositories(repositories); 93 | } 94 | 95 | 96 | @Timed 97 | @GET 98 | @Path("/payment/commit") 99 | @CacheControl(noCache = true) 100 | public Response getCurrentCommitPrice(@QueryParam("format") @DefaultValue("png") String format) 101 | throws IOException 102 | { 103 | CurrentPayment currentPayment = coinbaseManager.getCurrentPaymentAmount(); 104 | 105 | switch (format) { 106 | case "json": 107 | return Response.ok(currentPayment.getEntity(), MediaType.APPLICATION_JSON_TYPE).build(); 108 | case "png_small": 109 | return Response.ok(currentPayment.getSmallBadge(), "image/png").build(); 110 | default: 111 | return Response.ok(currentPayment.getBadge(), "image/png").build(); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/controllers/UnauthorizedHookException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.controllers; 19 | 20 | public class UnauthorizedHookException extends Throwable { 21 | public UnauthorizedHookException(String s) { 22 | super(s); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Author.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.entities; 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import org.hibernate.validator.constraints.NotEmpty; 23 | 24 | @JsonIgnoreProperties(ignoreUnknown = true) 25 | public class Author { 26 | 27 | @JsonProperty 28 | private String name; 29 | 30 | @JsonProperty 31 | @NotEmpty 32 | private String email; 33 | 34 | @JsonProperty 35 | private String username; 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public String getEmail() { 42 | return email; 43 | } 44 | 45 | public String getUsername() { 46 | return username; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Commit.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.entities; 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | 23 | import javax.validation.constraints.NotNull; 24 | 25 | @JsonIgnoreProperties(ignoreUnknown = true) 26 | public class Commit { 27 | 28 | @JsonProperty 29 | private String id; 30 | 31 | @JsonProperty 32 | private String message; 33 | 34 | @JsonProperty 35 | @NotNull 36 | private Author author; 37 | 38 | @JsonProperty 39 | private String url; 40 | 41 | @JsonProperty 42 | private boolean distinct; 43 | 44 | public String getSha() { 45 | return id; 46 | } 47 | 48 | public String getMessage() { 49 | return message; 50 | } 51 | 52 | public Author getAuthor() { 53 | return author; 54 | } 55 | 56 | public String getUrl() { 57 | return url; 58 | } 59 | 60 | public boolean isDistinct() { 61 | return distinct; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/CommitComment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.entities; 19 | 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | public class CommitComment { 23 | 24 | @JsonProperty 25 | private String body; 26 | 27 | public CommitComment(String body) { 28 | this.body = body; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Payment.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.entities; 19 | 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | public class Payment { 23 | @JsonProperty 24 | private String payment; 25 | 26 | public Payment(String payment) { 27 | this.payment = payment; 28 | } 29 | 30 | public String getPayment() { 31 | return payment; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/PushEvent.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.entities; 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | 23 | import javax.validation.constraints.NotNull; 24 | import java.util.List; 25 | 26 | @JsonIgnoreProperties(ignoreUnknown = true) 27 | public class PushEvent { 28 | 29 | @JsonProperty 30 | private String head; 31 | 32 | @JsonProperty 33 | private String ref; 34 | 35 | @JsonProperty 36 | private int size; 37 | 38 | @JsonProperty 39 | @NotNull 40 | List commits; 41 | 42 | @JsonProperty 43 | @NotNull 44 | Repository repository; 45 | 46 | public Repository getRepository() { 47 | return repository; 48 | } 49 | 50 | public String getHead() { 51 | return head; 52 | } 53 | 54 | public String getRef() { 55 | return ref; 56 | } 57 | 58 | public int getSize() { 59 | return size; 60 | } 61 | 62 | public List getCommits() { 63 | return commits; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Repositories.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.entities; 2 | 3 | import java.util.List; 4 | 5 | public class Repositories { 6 | 7 | public List repositories; 8 | 9 | public Repositories() {} 10 | 11 | public Repositories(List repositories) { 12 | this.repositories = repositories; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Repository.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.entities; 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import org.hibernate.validator.constraints.NotEmpty; 23 | 24 | import javax.validation.constraints.NotNull; 25 | 26 | @JsonIgnoreProperties(ignoreUnknown = true) 27 | public class Repository { 28 | 29 | @JsonProperty 30 | @NotEmpty 31 | private String url; 32 | 33 | @JsonProperty 34 | @NotNull 35 | private Author owner; 36 | 37 | @JsonProperty 38 | @NotEmpty 39 | private String name; 40 | 41 | @JsonProperty 42 | private String description; 43 | 44 | public Repository() {} 45 | 46 | public Repository(String url) { 47 | this.url = url; 48 | } 49 | 50 | public Author getOwner() { 51 | return owner; 52 | } 53 | 54 | public String getName() { 55 | return name; 56 | } 57 | 58 | public String getUrl() { 59 | return url; 60 | } 61 | 62 | public String getDescription() { 63 | return description; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Transaction.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.entities; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class Transaction { 6 | 7 | @JsonProperty 8 | private String destination; 9 | 10 | @JsonProperty 11 | private String amount; 12 | 13 | @JsonProperty 14 | private String commitUrl; 15 | 16 | @JsonProperty 17 | private String commitSha; 18 | 19 | @JsonProperty 20 | private String timestamp; 21 | 22 | @JsonProperty 23 | private String description; 24 | 25 | public Transaction() {} 26 | 27 | public Transaction(String destination, String amount, String commitUrl, 28 | String commitSha, String timestamp, String description) 29 | { 30 | this.destination = destination; 31 | this.amount = amount; 32 | this.commitUrl = commitUrl; 33 | this.commitSha = commitSha; 34 | this.timestamp = timestamp; 35 | this.description = description; 36 | } 37 | 38 | public String getDestination() { 39 | return destination; 40 | } 41 | 42 | public String getAmount() { 43 | return amount; 44 | } 45 | 46 | public String getCommitUrl() { 47 | return commitUrl; 48 | } 49 | 50 | public String getCommitSha() { 51 | return commitSha; 52 | } 53 | 54 | public String getTimestamp() { 55 | return timestamp; 56 | } 57 | 58 | public String getDescription() { 59 | return description; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/entities/Transactions.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.entities; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.util.List; 7 | 8 | public class Transactions { 9 | 10 | @JsonProperty 11 | private List transactions; 12 | 13 | public Transactions() {} 14 | 15 | public Transactions(List transactions) { 16 | this.transactions = transactions; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/mappers/IOExceptionMapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.mappers; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import javax.ws.rs.core.Response; 24 | import javax.ws.rs.ext.ExceptionMapper; 25 | import javax.ws.rs.ext.Provider; 26 | import java.io.IOException; 27 | 28 | @Provider 29 | public class IOExceptionMapper implements ExceptionMapper { 30 | 31 | private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class); 32 | 33 | @Override 34 | public Response toResponse(IOException e) { 35 | logger.warn("IOExceptionMapper", e); 36 | return Response.status(503).build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/mappers/UnauthorizedHookExceptionMapper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.mappers; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.whispersystems.bithub.controllers.UnauthorizedHookException; 23 | 24 | import javax.ws.rs.core.Response; 25 | import javax.ws.rs.ext.ExceptionMapper; 26 | import javax.ws.rs.ext.Provider; 27 | 28 | @Provider 29 | public class UnauthorizedHookExceptionMapper implements ExceptionMapper { 30 | 31 | private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class); 32 | 33 | @Override 34 | public Response toResponse(UnauthorizedHookException e) { 35 | logger.warn("IOExceptionMapper", e); 36 | return Response.status(401).build(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/storage/CacheManager.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.storage; 2 | 3 | import com.coinbase.api.exception.CoinbaseException; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.whispersystems.bithub.client.CoinbaseClient; 7 | import org.whispersystems.bithub.client.GithubClient; 8 | import org.whispersystems.bithub.config.RepositoryConfiguration; 9 | import org.whispersystems.bithub.entities.Payment; 10 | import org.whispersystems.bithub.entities.Repository; 11 | import org.whispersystems.bithub.entities.Transaction; 12 | import org.whispersystems.bithub.util.Badge; 13 | 14 | import java.io.IOException; 15 | import java.math.BigDecimal; 16 | import java.math.RoundingMode; 17 | import java.text.ParseException; 18 | import java.util.LinkedList; 19 | import java.util.List; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.ScheduledExecutorService; 22 | import java.util.concurrent.TimeUnit; 23 | import java.util.concurrent.atomic.AtomicReference; 24 | 25 | import io.dropwizard.lifecycle.Managed; 26 | 27 | public class CacheManager implements Managed { 28 | 29 | private static final int UPDATE_FREQUENCY_MILLIS = 60 * 1000; 30 | 31 | private final Logger logger = LoggerFactory.getLogger(CacheManager.class); 32 | private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); 33 | 34 | private final CoinbaseClient coinbaseClient; 35 | private final GithubClient githubClient; 36 | private final BigDecimal payoutRate; 37 | private final List repositories; 38 | 39 | private AtomicReference cachedPaymentStatus; 40 | private AtomicReference> cachedTransactions; 41 | private AtomicReference> cachedRepositories; 42 | 43 | public CacheManager(CoinbaseClient coinbaseClient, 44 | GithubClient githubClient, 45 | List repositories, 46 | BigDecimal payoutRate) 47 | { 48 | this.coinbaseClient = coinbaseClient; 49 | this.githubClient = githubClient; 50 | this.payoutRate = payoutRate; 51 | this.repositories = repositories; 52 | } 53 | 54 | @Override 55 | public void start() throws Exception { 56 | this.cachedPaymentStatus = new AtomicReference<>(createCurrentPaymentForBalance(coinbaseClient)); 57 | this.cachedTransactions = new AtomicReference<>(createRecentTransactions(coinbaseClient)); 58 | this.cachedRepositories = new AtomicReference<>(createRepositories(githubClient, repositories)); 59 | 60 | initializeUpdates(coinbaseClient, githubClient, repositories); 61 | } 62 | 63 | @Override 64 | public void stop() throws Exception { 65 | this.executor.shutdownNow(); 66 | } 67 | 68 | public List getRecentTransactions() { 69 | return cachedTransactions.get(); 70 | } 71 | 72 | public CurrentPayment getCurrentPaymentAmount() { 73 | return cachedPaymentStatus.get(); 74 | } 75 | 76 | public List getRepositories() { 77 | return cachedRepositories.get(); 78 | } 79 | 80 | public void initializeUpdates(final CoinbaseClient coinbaseClient, 81 | final GithubClient githubClient, 82 | final List repoConfigs) 83 | { 84 | executor.scheduleAtFixedRate(new Runnable() { 85 | @Override 86 | public void run() { 87 | logger.warn("Running cache update..."); 88 | try { 89 | CurrentPayment currentPayment = createCurrentPaymentForBalance(coinbaseClient); 90 | List transactions = createRecentTransactions (coinbaseClient); 91 | List repositories = createRepositories(githubClient, repoConfigs); 92 | 93 | cachedPaymentStatus.set(currentPayment); 94 | cachedTransactions.set(transactions); 95 | cachedRepositories.set(repositories); 96 | 97 | } catch (IOException | CoinbaseException e) { 98 | logger.warn("Failed to update badge", e); 99 | } 100 | } 101 | }, UPDATE_FREQUENCY_MILLIS, UPDATE_FREQUENCY_MILLIS, TimeUnit.MILLISECONDS); 102 | } 103 | 104 | private List createRepositories(GithubClient githubClient, 105 | List configured) 106 | { 107 | List repositoryList = new LinkedList<>(); 108 | 109 | for (RepositoryConfiguration repository : configured) { 110 | repositoryList.add(githubClient.getRepository(repository.getUrl())); 111 | } 112 | 113 | return repositoryList; 114 | } 115 | 116 | private CurrentPayment createCurrentPaymentForBalance(CoinbaseClient coinbaseClient) 117 | throws IOException, CoinbaseException 118 | { 119 | BigDecimal currentBalance = coinbaseClient.getAccountBalance(); 120 | BigDecimal paymentBtc = currentBalance.multiply(payoutRate); 121 | BigDecimal exchangeRate = coinbaseClient.getExchangeRate(); 122 | BigDecimal paymentUsd = paymentBtc.multiply(exchangeRate); 123 | 124 | paymentUsd = paymentUsd.setScale(2, RoundingMode.CEILING); 125 | return new CurrentPayment(Badge.createFor(paymentUsd.toPlainString()), 126 | Badge.createSmallFor(paymentUsd.toPlainString()), 127 | new Payment(paymentUsd.toPlainString())); 128 | } 129 | 130 | private List createRecentTransactions(CoinbaseClient coinbaseClient) 131 | throws IOException, CoinbaseException 132 | { 133 | List recentTransactions = coinbaseClient.getRecentTransactions(); 134 | BigDecimal exchangeRate = coinbaseClient.getExchangeRate(); 135 | List transactions = new LinkedList<>(); 136 | 137 | for (com.coinbase.api.entity.Transaction coinbaseTransaction : recentTransactions) { 138 | try { 139 | if (isSentTransaction(coinbaseTransaction)) { 140 | CoinbaseTransactionParser parser = new CoinbaseTransactionParser(coinbaseTransaction); 141 | String url = parser.parseUrlFromMessage(); 142 | String sha = parser.parseShaFromUrl(url); 143 | String description = githubClient.getCommitDescription(url); 144 | 145 | transactions.add(new Transaction(parser.parseDestinationFromMessage(), 146 | parser.parseAmountInDollars(exchangeRate), 147 | url, sha, parser.parseTimestamp(), 148 | description)); 149 | 150 | if (transactions.size() >= 10) 151 | break; 152 | } 153 | } catch (ParseException e) { 154 | logger.warn("Parse", e); 155 | } 156 | } 157 | 158 | return transactions; 159 | } 160 | 161 | private boolean isSentTransaction(com.coinbase.api.entity.Transaction coinbaseTransaction) { 162 | BigDecimal amount = coinbaseTransaction.getAmount().getAmount(); 163 | return amount.compareTo(new BigDecimal(0.0)) < 0; 164 | } 165 | 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/storage/CoinbaseTransactionParser.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.storage; 2 | 3 | import com.coinbase.api.entity.Transaction; 4 | import org.apache.commons.lang3.StringEscapeUtils; 5 | import org.joda.time.DateTime; 6 | import org.joda.time.format.DateTimeFormat; 7 | import org.joda.time.format.DateTimeFormatter; 8 | 9 | import java.math.BigDecimal; 10 | import java.math.RoundingMode; 11 | import java.text.ParseException; 12 | 13 | public class CoinbaseTransactionParser { 14 | 15 | private final Transaction coinbaseTransaction; 16 | 17 | public CoinbaseTransactionParser(Transaction coinbaseTransaction) { 18 | this.coinbaseTransaction = coinbaseTransaction; 19 | } 20 | 21 | public String parseAmountInDollars(BigDecimal exchangeRate) { 22 | return coinbaseTransaction.getAmount().getAmount().abs() 23 | .multiply(exchangeRate) 24 | .setScale(2, RoundingMode.CEILING) 25 | .toPlainString(); 26 | } 27 | 28 | public String parseTimestamp() throws ParseException { 29 | DateTime timestamp = coinbaseTransaction.getCreatedAt(); 30 | DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZ"); 31 | return fmt.print(timestamp); 32 | } 33 | 34 | public String parseDestinationFromMessage() { 35 | String message = StringEscapeUtils.unescapeHtml4(coinbaseTransaction.getNotes()); 36 | int startToken = message.indexOf("__"); 37 | 38 | if (startToken == -1) { 39 | return "Unknown"; 40 | } 41 | 42 | int endToken = message.indexOf("__", startToken + 1); 43 | 44 | if (endToken == -1) { 45 | return "Unknown"; 46 | } 47 | 48 | return message.substring(startToken+2, endToken); 49 | } 50 | 51 | public String parseUrlFromMessage() throws ParseException { 52 | String message = StringEscapeUtils.unescapeHtml4(coinbaseTransaction.getNotes()); 53 | int urlIndex = message.indexOf("https://"); 54 | 55 | return message.substring(urlIndex).trim(); 56 | } 57 | 58 | public String parseShaFromUrl(String url) throws ParseException { 59 | if (url == null) { 60 | throw new ParseException("No url", 0); 61 | } 62 | 63 | String[] parts = url.split("/"); 64 | String fullHash = parts[parts.length-1]; 65 | 66 | if (fullHash.length() < 8) { 67 | throw new ParseException("Not long enough", 0); 68 | } 69 | 70 | return fullHash.substring(0, 8); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/storage/CurrentPayment.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.storage; 2 | 3 | import org.whispersystems.bithub.entities.Payment; 4 | 5 | public class CurrentPayment { 6 | 7 | private final byte[] badge; 8 | private final byte[] smallBadge; 9 | private final Payment entity; 10 | 11 | protected CurrentPayment(byte[] badge, byte[] smallBadge, Payment entity) { 12 | this.badge = badge; 13 | this.smallBadge = smallBadge; 14 | this.entity = entity; 15 | } 16 | 17 | public byte[] getBadge() { 18 | return badge; 19 | } 20 | 21 | public byte[] getSmallBadge() { 22 | return smallBadge; 23 | } 24 | 25 | public Payment getEntity() { 26 | return entity; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/util/AdvancedAtomicLong.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.util; 19 | 20 | import java.util.concurrent.atomic.AtomicLong; 21 | 22 | public class AdvancedAtomicLong extends AtomicLong { 23 | 24 | public AdvancedAtomicLong(long initial) { 25 | super(initial); 26 | } 27 | 28 | public boolean setIfGreater(long compare, long update) { 29 | while(true) { 30 | long current = get(); 31 | 32 | if (compare > current) { 33 | if (compareAndSet(current, update)) { 34 | return true; 35 | } 36 | } else { 37 | return false; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/util/Badge.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.util; 19 | 20 | import com.google.common.io.Resources; 21 | 22 | import javax.imageio.ImageIO; 23 | import java.awt.*; 24 | import java.awt.image.BufferedImage; 25 | import java.io.ByteArrayInputStream; 26 | import java.io.ByteArrayOutputStream; 27 | import java.io.IOException; 28 | 29 | public class Badge { 30 | 31 | public static byte[] createFor(String price) throws IOException { 32 | byte[] badgeBackground = Resources.toByteArray(Resources.getResource("assets/badge.png")); 33 | BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(badgeBackground)); 34 | Graphics2D graphics = bufferedImage.createGraphics(); 35 | 36 | graphics.setFont(new Font("OpenSans", Font.PLAIN, 34)); 37 | graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 38 | RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); 39 | graphics.drawString(price + " USD", 86, 45); 40 | graphics.dispose(); 41 | 42 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 43 | ImageIO.write(bufferedImage, "png", baos); 44 | 45 | return baos.toByteArray(); 46 | } 47 | 48 | public static byte[] createSmallFor(String price) throws IOException { 49 | byte[] badgeBackground = Resources.toByteArray(Resources.getResource("assets/badge-small.png")); 50 | BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(badgeBackground)); 51 | Graphics2D graphics = bufferedImage.createGraphics(); 52 | 53 | graphics.setFont(new Font("OpenSans", Font.PLAIN, 9)); 54 | graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 55 | RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); 56 | graphics.drawString(price + " USD", 22, 14); 57 | graphics.dispose(); 58 | 59 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 60 | ImageIO.write(bufferedImage, "png", baos); 61 | 62 | return baos.toByteArray(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/views/DashboardView.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.views; 2 | 3 | import org.whispersystems.bithub.entities.Repository; 4 | import org.whispersystems.bithub.entities.Transaction; 5 | import org.whispersystems.bithub.storage.CurrentPayment; 6 | 7 | import java.util.List; 8 | 9 | import io.dropwizard.views.View; 10 | 11 | public class DashboardView extends View { 12 | 13 | private final String organizationName; 14 | private final String donationUrl; 15 | private final CurrentPayment currentPayment; 16 | private final List repositories; 17 | private final List transactions; 18 | 19 | public DashboardView(String organizationName, String donationUrl, 20 | CurrentPayment currentPayment, 21 | List repositories, 22 | List transactions) 23 | { 24 | super("dashboard.mustache"); 25 | this.organizationName = organizationName; 26 | this.donationUrl = donationUrl; 27 | this.currentPayment = currentPayment; 28 | this.repositories = repositories; 29 | this.transactions = transactions; 30 | } 31 | 32 | public String getPayment() { 33 | return currentPayment.getEntity().getPayment(); 34 | } 35 | 36 | public String getOrganizationName() { 37 | return organizationName; 38 | } 39 | 40 | public String getDonationUrl() { 41 | return donationUrl; 42 | } 43 | 44 | public List getRepositories() { 45 | return repositories; 46 | } 47 | 48 | public List getTransactions() { 49 | return transactions; 50 | } 51 | 52 | public String getRepositoriesCount() { 53 | return String.valueOf(repositories.size()); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/whispersystems/bithub/views/TransactionsView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.views; 19 | 20 | import org.whispersystems.bithub.entities.Transaction; 21 | 22 | import java.util.List; 23 | 24 | import io.dropwizard.views.View; 25 | 26 | /** 27 | * A rendered HTML view of recent BitHub transactions. 28 | * 29 | * @author Moxie Marlinspike 30 | */ 31 | public class TransactionsView extends View { 32 | 33 | private final List transactions; 34 | 35 | public TransactionsView(List transactions) { 36 | super("recent_transactions.mustache"); 37 | this.transactions = transactions; 38 | } 39 | 40 | public List getTransactions() { 41 | return transactions; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/assets/badge-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalapp/BitHub/82c9d21d292772e35ac40c8269001add2aa3e884/src/main/resources/assets/badge-small.png -------------------------------------------------------------------------------- /src/main/resources/assets/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalapp/BitHub/82c9d21d292772e35ac40c8269001add2aa3e884/src/main/resources/assets/badge.png -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 888888b. d8b 888 888 888 888 2 | 888 "88b Y8P 888 888 888 888 3 | 888 .88P 888 888 888 888 4 | 8888888K. 888 888888 8888888888 888 888 88888b. 5 | 888 "Y88b 888 888 888 888 888 888 888 "88b 6 | 888 888 888 888 888 888 888 888 888 888 7 | 888 d88P 888 Y88b. 888 888 Y88b 888 888 d88P 8 | 8888888P" 888 "Y888 888 888 "Y88888 88888P" 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/org/whispersystems/bithub/views/dashboard.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | BitHub :: Dashboard 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | BitHub 81 | {{organizationName}} 82 | Donate BTC today 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {{payment}} 91 | USD per commit 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 0 102 | open bounties 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {{repositoriesCount}} 113 | repositories 114 | 115 | 116 | 117 | 118 | 119 | Recent payments 120 | 121 | 122 | 123 | 124 | USD 125 | Author 126 | Description 127 | Commit 128 | 129 | 130 | 131 | {{#transactions}} 132 | 133 | ${{amount}} USD 134 | {{destination}} 135 | {{description}} 136 | {{commitSha}} 137 | 138 | {{/transactions}} 139 | 140 | 141 | 142 | 143 | Repositories 144 | 145 | {{#repositories}} 146 | 147 | 148 | 149 | {{name}} 150 | 151 | 152 | {{description}} 153 | 154 | 155 | 156 | {{/repositories}} 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /src/main/resources/org/whispersystems/bithub/views/recent_transactions.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | 44 | 45 | 46 | {{#transactions}} 47 | Sent ${{amount}} USD to {{destination}} for {{commitSha}} {{timestamp}}. 48 | {{/transactions}} 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013 Open WhisperSystems 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package org.whispersystems.bithub.tests.controllers; 19 | 20 | import com.sun.jersey.api.client.ClientResponse; 21 | import com.sun.jersey.core.util.MultivaluedMapImpl; 22 | import org.apache.commons.codec.binary.Base64; 23 | import org.junit.Before; 24 | import org.junit.Rule; 25 | import org.junit.Test; 26 | import org.whispersystems.bithub.auth.GithubWebhookAuthenticator; 27 | import org.whispersystems.bithub.client.CoinbaseClient; 28 | import org.whispersystems.bithub.client.GithubClient; 29 | import org.whispersystems.bithub.client.TransferFailedException; 30 | import org.whispersystems.bithub.config.RepositoryConfiguration; 31 | import org.whispersystems.bithub.controllers.GithubController; 32 | import org.whispersystems.bithub.entities.Author; 33 | import org.whispersystems.bithub.mappers.UnauthorizedHookExceptionMapper; 34 | 35 | import javax.ws.rs.core.MediaType; 36 | import java.io.InputStream; 37 | import java.math.BigDecimal; 38 | import java.util.LinkedList; 39 | import java.util.List; 40 | import java.util.Scanner; 41 | 42 | import io.dropwizard.auth.basic.BasicAuthProvider; 43 | import io.dropwizard.testing.junit.ResourceTestRule; 44 | import static org.fest.assertions.api.Assertions.assertThat; 45 | import static org.mockito.Matchers.any; 46 | import static org.mockito.Matchers.anyString; 47 | import static org.mockito.Matchers.eq; 48 | import static org.mockito.Mockito.*; 49 | 50 | public class GithubControllerTest { 51 | 52 | private static final BigDecimal BALANCE = new BigDecimal(10.01); 53 | private static final BigDecimal EXCHANGE_RATE = new BigDecimal(1.0); 54 | 55 | private final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class); 56 | private final GithubClient githubClient = mock(GithubClient.class); 57 | 58 | // HTTP Basic Authentication data 59 | private final String authUsername = "TestUser"; 60 | private final String authPassword = "TestPassword"; 61 | private final String authRealm = GithubWebhookAuthenticator.REALM; 62 | private final String authString = "Basic " + Base64.encodeBase64String((authUsername + ":" + authPassword).getBytes()); 63 | private final String invalidUserAuthString = "Basic " + Base64.encodeBase64(("wrong:" + authPassword).getBytes()); 64 | private final String invalidPasswordAuthString = "Basic " + Base64.encodeBase64((authUsername + ":wrong").getBytes()); 65 | 66 | private final List repositories = new LinkedList() {{ 67 | add(new RepositoryConfiguration("https://github.com/moxie0/test")); 68 | add(new RepositoryConfiguration("https://github.com/moxie0/optin", "FREEBIE")); 69 | }}; 70 | 71 | @Rule 72 | public final ResourceTestRule resources = ResourceTestRule.builder() 73 | .addProvider(new UnauthorizedHookExceptionMapper()) 74 | .addProvider(new BasicAuthProvider<>(new GithubWebhookAuthenticator(authUsername, authPassword), authRealm)) 75 | .addResource(new GithubController(repositories, githubClient, coinbaseClient, new BigDecimal(0.02))) 76 | .build(); 77 | 78 | 79 | @Before 80 | public void setup() throws Exception, TransferFailedException { 81 | when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE); 82 | when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE); 83 | } 84 | 85 | protected String payload(String path) { 86 | InputStream is = this.getClass().getResourceAsStream(path); 87 | Scanner s = new Scanner(is).useDelimiter("\\A"); 88 | return s.hasNext() ? s.next() : ""; 89 | } 90 | 91 | @Test 92 | public void testInvalidRepository() throws Exception { 93 | String payloadValue = payload("/payloads/invalid_repo.json"); 94 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 95 | post.add("payload", payloadValue); 96 | ClientResponse response = resources.client().resource("/v1/github/commits/") 97 | .header("X-Forwarded-For", "192.30.252.1") 98 | .header("Authorization", authString) 99 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 100 | .post(ClientResponse.class, post); 101 | 102 | assertThat(response.getStatus()).isEqualTo(401); 103 | } 104 | 105 | @Test 106 | public void testInvalidOrigin() throws Exception { 107 | String payloadValue = payload("/payloads/invalid_origin.json"); 108 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 109 | post.add("payload", payloadValue); 110 | ClientResponse response = resources.client().resource("/v1/github/commits/") 111 | .header("X-Forwarded-For", "192.30.242.1") 112 | .header("Authorization", authString) 113 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 114 | .post(ClientResponse.class, post); 115 | 116 | assertThat(response.getStatus()).isEqualTo(401); 117 | } 118 | 119 | @Test 120 | public void testMissingAuth() throws Exception, TransferFailedException { 121 | String payloadValue = payload("/payloads/valid_commit.json"); 122 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 123 | post.add("payload", payloadValue); 124 | ClientResponse response = resources.client().resource("/v1/github/commits/") 125 | .header("X-Forwarded-For", "192.30.252.1") 126 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 127 | .post(ClientResponse.class, post); 128 | 129 | assertThat(response.getStatus()).isEqualTo(401); 130 | } 131 | 132 | @Test 133 | public void testInvalidAuthUser() throws Exception, TransferFailedException { 134 | String payloadValue = payload("/payloads/valid_commit.json"); 135 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 136 | post.add("payload", payloadValue); 137 | ClientResponse response = resources.client().resource("/v1/github/commits/") 138 | .header("X-Forwarded-For", "192.30.252.1") 139 | .header("Authorization", invalidUserAuthString) 140 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 141 | .post(ClientResponse.class, post); 142 | 143 | assertThat(response.getStatus()).isEqualTo(401); 144 | } 145 | 146 | @Test 147 | public void testInvalidAuthPassword() throws Exception, TransferFailedException { 148 | String payloadValue = payload("/payloads/valid_commit.json"); 149 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 150 | post.add("payload", payloadValue); 151 | ClientResponse response = resources.client().resource("/v1/github/commits/") 152 | .header("X-Forwarded-For", "192.30.252.1") 153 | .header("Authorization", invalidPasswordAuthString) 154 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 155 | .post(ClientResponse.class, post); 156 | 157 | assertThat(response.getStatus()).isEqualTo(401); 158 | } 159 | 160 | @Test 161 | public void testOptOutCommit() throws Exception, TransferFailedException { 162 | String payloadValue = payload("/payloads/opt_out_commit.json"); 163 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 164 | post.add("payload", payloadValue); 165 | ClientResponse response = resources.client().resource("/v1/github/commits/") 166 | .header("X-Forwarded-For", "192.30.252.1") 167 | .header("Authorization", authString) 168 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 169 | .post(ClientResponse.class, post); 170 | 171 | verify(coinbaseClient, never()).sendPayment(any(Author.class), 172 | any(BigDecimal.class), 173 | anyString()); 174 | } 175 | 176 | @Test 177 | public void testValidCommit() throws Exception, TransferFailedException { 178 | String payloadValue = payload("/payloads/valid_commit.json"); 179 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 180 | post.add("payload", payloadValue); 181 | ClientResponse response = resources.client().resource("/v1/github/commits/") 182 | .header("X-Forwarded-For", "192.30.252.1") 183 | .header("Authorization", authString) 184 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 185 | .post(ClientResponse.class, post); 186 | 187 | verify(coinbaseClient).sendPayment(any(Author.class), 188 | eq(BALANCE.multiply(new BigDecimal(0.02))), 189 | anyString()); 190 | } 191 | 192 | @Test 193 | public void testNonMaster() throws Exception, TransferFailedException { 194 | String payloadValue = payload("/payloads/non_master_push.json"); 195 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 196 | post.add("payload", payloadValue); 197 | ClientResponse response = resources.client().resource("/v1/github/commits/") 198 | .header("X-Forwarded-For", "192.30.252.1") 199 | .header("Authorization", authString) 200 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 201 | .post(ClientResponse.class, post); 202 | 203 | verify(coinbaseClient, never()).sendPayment(any(Author.class), 204 | eq(BALANCE.multiply(new BigDecimal(0.02))), 205 | anyString()); 206 | } 207 | 208 | @Test 209 | public void testValidMultipleCommitsMultipleAuthors() throws Exception, TransferFailedException { 210 | String payloadValue = payload("/payloads/multiple_commits_authors.json"); 211 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 212 | post.add("payload", payloadValue); 213 | ClientResponse response = resources.client().resource("/v1/github/commits/") 214 | .header("X-Forwarded-For", "192.30.252.1") 215 | .header("Authorization", authString) 216 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 217 | .post(ClientResponse.class, post); 218 | 219 | verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))), 220 | anyString()); 221 | verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.subtract(BALANCE.multiply(new BigDecimal(0.02))) 222 | .multiply(new BigDecimal(0.02))), anyString()); 223 | } 224 | 225 | @Test 226 | public void testOptInCommit() throws Exception, TransferFailedException { 227 | String payloadValue = payload("/payloads/opt_in_commit.json"); 228 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 229 | post.add("payload", payloadValue); 230 | ClientResponse response = resources.client().resource("/v1/github/commits/") 231 | .header("X-Forwarded-For", "192.30.252.1") 232 | .header("Authorization", authString) 233 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 234 | .post(ClientResponse.class, post); 235 | 236 | verify(coinbaseClient).sendPayment(any(Author.class), 237 | eq(BALANCE.multiply(new BigDecimal(0.02))), 238 | anyString()); 239 | } 240 | 241 | @Test 242 | public void testNoOptInCommit() throws Exception, TransferFailedException { 243 | String payloadValue = payload("/payloads/no_opt_in_commit.json"); 244 | MultivaluedMapImpl post = new MultivaluedMapImpl(); 245 | post.add("payload", payloadValue); 246 | ClientResponse response = resources.client().resource("/v1/github/commits/") 247 | .header("X-Forwarded-For", "192.30.252.1") 248 | .header("Authorization", authString) 249 | .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) 250 | .post(ClientResponse.class, post); 251 | 252 | verify(coinbaseClient, never()).sendPayment(any(Author.class), 253 | any(BigDecimal.class), 254 | anyString()); 255 | } 256 | 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/test/java/org/whispersystems/bithub/tests/controllers/StatusControllerTest.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.tests.controllers; 2 | 3 | import com.coinbase.api.ObjectMapperProvider; 4 | import com.coinbase.api.entity.TransactionsResponse; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.sun.jersey.api.client.ClientResponse; 7 | import org.junit.ClassRule; 8 | import org.junit.Test; 9 | import org.whispersystems.bithub.client.CoinbaseClient; 10 | import org.whispersystems.bithub.client.GithubClient; 11 | import org.whispersystems.bithub.config.RepositoryConfiguration; 12 | import org.whispersystems.bithub.controllers.StatusController; 13 | import org.whispersystems.bithub.storage.CacheManager; 14 | 15 | import javax.ws.rs.core.MediaType; 16 | import java.math.BigDecimal; 17 | import java.util.LinkedList; 18 | 19 | import io.dropwizard.testing.junit.ResourceTestRule; 20 | import static org.fest.assertions.api.Assertions.assertThat; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.when; 23 | 24 | public class StatusControllerTest { 25 | 26 | private static final BigDecimal PAYOUT_RATE = new BigDecimal(0.02 ); 27 | private static final BigDecimal BALANCE = new BigDecimal(10.01); 28 | private static final BigDecimal EXCHANGE_RATE = new BigDecimal(1.0 ); 29 | 30 | private static final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class); 31 | private static final GithubClient githubClient = mock(GithubClient.class ); 32 | 33 | @ClassRule 34 | public static ResourceTestRule resources; 35 | 36 | static { 37 | try { 38 | ObjectMapper objectMapper = ObjectMapperProvider.createDefaultMapper(); 39 | TransactionsResponse transactionsResponse = objectMapper.readValue(StatusControllerTest.class.getResourceAsStream("/payloads/transactions.json"), TransactionsResponse.class); 40 | when(coinbaseClient.getRecentTransactions()).thenReturn(transactionsResponse.getTransactions()); 41 | when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE); 42 | when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE); 43 | 44 | CacheManager coinbaseManager = new CacheManager(coinbaseClient, githubClient, 45 | new LinkedList(), 46 | PAYOUT_RATE); 47 | coinbaseManager.start(); 48 | 49 | resources = ResourceTestRule.builder() 50 | .addResource(new StatusController(coinbaseManager, null)) 51 | .build(); 52 | } catch (Exception e) { 53 | throw new AssertionError(e); 54 | } 55 | } 56 | 57 | // @Before 58 | // public void setup() throws Exception { 59 | // when(coinbaseClient.getRecentTransactions()).thenReturn(fromJson(jsonFixture("payloads/transactions.json"), RecentTransactionsResponse.class).getTransactions()); 60 | // when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE); 61 | // when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE); 62 | // 63 | // } 64 | 65 | // @Test 66 | // public void testTransactionsHtml() throws Exception { 67 | // ClientResponse response = resources.client().resource("/v1/status/transactions/") 68 | // .get(ClientResponse.class); 69 | // 70 | // assertThat(response.getStatus()).isEqualTo(200); 71 | // assertThat(response.getType()).isEqualTo(MediaType.TEXT_HTML_TYPE); 72 | // } 73 | 74 | @Test 75 | public void testTransactionsJson() throws Exception { 76 | ClientResponse response = resources.client().resource("/v1/status/transactions/?format=json").accept(MediaType.APPLICATION_JSON_TYPE) 77 | .get(ClientResponse.class); 78 | 79 | assertThat(response.getStatus()).isEqualTo(200); 80 | assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/org/whispersystems/bithub/tests/util/JsonHelper.java: -------------------------------------------------------------------------------- 1 | package org.whispersystems.bithub.tests.util; 2 | 3 | 4 | import com.fasterxml.jackson.core.JsonParseException; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | 9 | import java.io.IOException; 10 | 11 | import static io.dropwizard.testing.FixtureHelpers.fixture; 12 | 13 | public class JsonHelper { 14 | 15 | private static final ObjectMapper objectMapper = new ObjectMapper(); 16 | 17 | public static String asJson(Object object) throws JsonProcessingException { 18 | return objectMapper.writeValueAsString(object); 19 | } 20 | 21 | public static T fromJson(String value, Class clazz) throws IOException { 22 | return objectMapper.readValue(value, clazz); 23 | } 24 | 25 | public static String jsonFixture(String filename) throws IOException { 26 | return objectMapper.writeValueAsString(objectMapper.readValue(fixture(filename), JsonNode.class)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/payloads/invalid_origin.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "100e9859651b35a3505cc278e9a98a076f79940b", 3 | "before": "6626766348ab245bdb3351989f753bd6e792524a", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "info@whispersystems.org", 9 | "name": "WhisperBTC", 10 | "username": "WhisperBTC" 11 | }, 12 | "committer": { 13 | "email": "info@whispersystems.org", 14 | "name": "WhisperBTC", 15 | "username": "WhisperBTC" 16 | }, 17 | "distinct": true, 18 | "id": "fd7daeb1de6d72220b1313a7f1112d43885013aa", 19 | "message": "Update foo", 20 | "modified": [ 21 | "foo" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:27:00-08:00", 25 | "url": "https://github.com/moxie0/tempt/commit/fd7daeb1de6d72220b1313a7f1112d43885013aa" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "100e9859651b35a3505cc278e9a98a076f79940b", 41 | "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", 42 | "modified": [ 43 | "foo" 44 | ], 45 | "removed": [], 46 | "timestamp": "2013-12-14T11:27:28-08:00", 47 | "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" 48 | } 49 | ], 50 | "compare": "https://github.com/moxie0/tempt/compare/6626766348ab...100e9859651b", 51 | "created": false, 52 | "deleted": false, 53 | "forced": false, 54 | "head_commit": { 55 | "added": [], 56 | "author": { 57 | "email": "moxie@thoughtcrime.org", 58 | "name": "Moxie Marlinspike", 59 | "username": "moxie0" 60 | }, 61 | "committer": { 62 | "email": "moxie@thoughtcrime.org", 63 | "name": "Moxie Marlinspike", 64 | "username": "moxie0" 65 | }, 66 | "distinct": true, 67 | "id": "100e9859651b35a3505cc278e9a98a076f79940b", 68 | "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", 69 | "modified": [ 70 | "foo" 71 | ], 72 | "removed": [], 73 | "timestamp": "2013-12-14T11:27:28-08:00", 74 | "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" 75 | }, 76 | "pusher": { 77 | "email": "moxie@thoughtcrime.org", 78 | "name": "moxie0" 79 | }, 80 | "ref": "refs/heads/master", 81 | "repository": { 82 | "created_at": 1386866024, 83 | "description": "test", 84 | "fork": false, 85 | "forks": 1, 86 | "has_downloads": true, 87 | "has_issues": true, 88 | "has_wiki": true, 89 | "id": 15141344, 90 | "master_branch": "master", 91 | "name": "tempt", 92 | "open_issues": 0, 93 | "owner": { 94 | "email": "moxie@thoughtcrime.org", 95 | "name": "moxie0" 96 | }, 97 | "private": false, 98 | "pushed_at": 1387049248, 99 | "size": 216, 100 | "stargazers": 1, 101 | "url": "https://github.com/moxie0/test", 102 | "watchers": 1 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/resources/payloads/invalid_repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "100e9859651b35a3505cc278e9a98a076f79940b", 3 | "before": "6626766348ab245bdb3351989f753bd6e792524a", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "info@whispersystems.org", 9 | "name": "WhisperBTC", 10 | "username": "WhisperBTC" 11 | }, 12 | "committer": { 13 | "email": "info@whispersystems.org", 14 | "name": "WhisperBTC", 15 | "username": "WhisperBTC" 16 | }, 17 | "distinct": true, 18 | "id": "fd7daeb1de6d72220b1313a7f1112d43885013aa", 19 | "message": "Update foo", 20 | "modified": [ 21 | "foo" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:27:00-08:00", 25 | "url": "https://github.com/moxie0/tempt/commit/fd7daeb1de6d72220b1313a7f1112d43885013aa" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "100e9859651b35a3505cc278e9a98a076f79940b", 41 | "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", 42 | "modified": [ 43 | "foo" 44 | ], 45 | "removed": [], 46 | "timestamp": "2013-12-14T11:27:28-08:00", 47 | "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" 48 | } 49 | ], 50 | "compare": "https://github.com/moxie0/tempt/compare/6626766348ab...100e9859651b", 51 | "created": false, 52 | "deleted": false, 53 | "forced": false, 54 | "head_commit": { 55 | "added": [], 56 | "author": { 57 | "email": "moxie@thoughtcrime.org", 58 | "name": "Moxie Marlinspike", 59 | "username": "moxie0" 60 | }, 61 | "committer": { 62 | "email": "moxie@thoughtcrime.org", 63 | "name": "Moxie Marlinspike", 64 | "username": "moxie0" 65 | }, 66 | "distinct": true, 67 | "id": "100e9859651b35a3505cc278e9a98a076f79940b", 68 | "message": "Merge pull request #2 from WhisperBTC/patch-2\n\nUpdate foo", 69 | "modified": [ 70 | "foo" 71 | ], 72 | "removed": [], 73 | "timestamp": "2013-12-14T11:27:28-08:00", 74 | "url": "https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b" 75 | }, 76 | "pusher": { 77 | "email": "moxie@thoughtcrime.org", 78 | "name": "moxie0" 79 | }, 80 | "ref": "refs/heads/master", 81 | "repository": { 82 | "created_at": 1386866024, 83 | "description": "test", 84 | "fork": false, 85 | "forks": 1, 86 | "has_downloads": true, 87 | "has_issues": true, 88 | "has_wiki": true, 89 | "id": 15141344, 90 | "master_branch": "master", 91 | "name": "tempt", 92 | "open_issues": 0, 93 | "owner": { 94 | "email": "moxie@thoughtcrime.org", 95 | "name": "moxie0" 96 | }, 97 | "private": false, 98 | "pushed_at": 1387049248, 99 | "size": 216, 100 | "stargazers": 1, 101 | "url": "https://github.com/moxie0/tempt", 102 | "watchers": 1 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/resources/payloads/multiple_commits_authors.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "1481a2de7b2a7d02428ad93446ab166be7793fbb", 3 | "before": "17c497ccc7cca9c2f735aa07e9e3813060ce9a6a", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "otherauthor@noway.biz", 9 | "name": "Garen Torikian", 10 | "username": "octokitty" 11 | }, 12 | "committer": { 13 | "email": "lolwut@noway.biz", 14 | "name": "Garen Torikian", 15 | "username": "octokitty" 16 | }, 17 | "distinct": true, 18 | "id": "c441029cf673f84c8b7db52d0a5944ee5c52ff89", 19 | "message": "Test", 20 | "modified": [ 21 | "README.md" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-02-22T13:50:07-08:00", 25 | "url": "https://github.com/octokitty/testing/commit/c441029cf673f84c8b7db52d0a5944ee5c52ff89" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "lolwut@noway.biz", 31 | "name": "Garen Torikian", 32 | "username": "octokitty" 33 | }, 34 | "committer": { 35 | "email": "lolwut@noway.biz", 36 | "name": "Garen Torikian", 37 | "username": "octokitty" 38 | }, 39 | "distinct": true, 40 | "id": "36c5f2243ed24de58284a96f2a643bed8c028658", 41 | "message": "This is me testing the windows client.", 42 | "modified": [ 43 | "README.md" 44 | ], 45 | "removed": [], 46 | "timestamp": "2013-02-22T14:07:13-08:00", 47 | "url": "https://github.com/octokitty/testing/commit/36c5f2243ed24de58284a96f2a643bed8c028658" 48 | }, 49 | { 50 | "added": [ 51 | "words/madame-bovary.txt" 52 | ], 53 | "author": { 54 | "email": "lolwut@noway.biz", 55 | "name": "Garen Torikian", 56 | "username": "octokitty" 57 | }, 58 | "committer": { 59 | "email": "lolwut@noway.biz", 60 | "name": "Garen Torikian", 61 | "username": "octokitty" 62 | }, 63 | "distinct": true, 64 | "id": "1481a2de7b2a7d02428ad93446ab166be7793fbb", 65 | "message": "Rename madame-bovary.txt to words/madame-bovary.txt", 66 | "modified": [], 67 | "removed": [ 68 | "madame-bovary.txt" 69 | ], 70 | "timestamp": "2013-03-12T08:14:29-07:00", 71 | "url": "https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb" 72 | } 73 | ], 74 | "compare": "https://github.com/octokitty/testing/compare/17c497ccc7cc...1481a2de7b2a", 75 | "created": false, 76 | "deleted": false, 77 | "forced": false, 78 | "head_commit": { 79 | "added": [ 80 | "words/madame-bovary.txt" 81 | ], 82 | "author": { 83 | "email": "lolwut@noway.biz", 84 | "name": "Garen Torikian", 85 | "username": "octokitty" 86 | }, 87 | "committer": { 88 | "email": "lolwut@noway.biz", 89 | "name": "Garen Torikian", 90 | "username": "octokitty" 91 | }, 92 | "distinct": true, 93 | "id": "1481a2de7b2a7d02428ad93446ab166be7793fbb", 94 | "message": "Rename madame-bovary.txt to words/madame-bovary.txt", 95 | "modified": [], 96 | "removed": [ 97 | "madame-bovary.txt" 98 | ], 99 | "timestamp": "2013-03-12T08:14:29-07:00", 100 | "url": "https://github.com/octokitty/testing/commit/1481a2de7b2a7d02428ad93446ab166be7793fbb" 101 | }, 102 | "pusher": { 103 | "email": "lolwut@noway.biz", 104 | "name": "Garen Torikian" 105 | }, 106 | "ref": "refs/heads/master", 107 | "repository": { 108 | "created_at": 1332977768, 109 | "description": "", 110 | "fork": false, 111 | "forks": 0, 112 | "has_downloads": true, 113 | "has_issues": true, 114 | "has_wiki": true, 115 | "homepage": "", 116 | "id": 3860742, 117 | "language": "Ruby", 118 | "master_branch": "master", 119 | "name": "testing", 120 | "open_issues": 2, 121 | "owner": { 122 | "email": "lolwut@noway.biz", 123 | "name": "octokitty" 124 | }, 125 | "private": false, 126 | "pushed_at": 1363295520, 127 | "size": 2156, 128 | "stargazers": 1, 129 | "url": "https://github.com/moxie0/test", 130 | "watchers": 1 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/resources/payloads/no_opt_in_commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 3 | "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "moxie@thoughtcrime.org", 9 | "name": "Moxie Marlinspike", 10 | "username": "moxie0" 11 | }, 12 | "committer": { 13 | "email": "moxie@thoughtcrime.org", 14 | "name": "Moxie Marlinspike", 15 | "username": "moxie0" 16 | }, 17 | "distinct": true, 18 | "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", 19 | "message": "Update path", 20 | "modified": [ 21 | "README.md" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:42:28-08:00", 25 | "url": "https://github.com/moxie0/optin/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 41 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 42 | "modified": [], 43 | "removed": [], 44 | "timestamp": "2013-12-14T11:42:44-08:00", 45 | "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 46 | } 47 | ], 48 | "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", 49 | "created": false, 50 | "deleted": false, 51 | "forced": false, 52 | "head_commit": { 53 | "added": [], 54 | "author": { 55 | "email": "moxie@thoughtcrime.org", 56 | "name": "Moxie Marlinspike", 57 | "username": "moxie0" 58 | }, 59 | "committer": { 60 | "email": "moxie@thoughtcrime.org", 61 | "name": "Moxie Marlinspike", 62 | "username": "moxie0" 63 | }, 64 | "distinct": true, 65 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 66 | "message": "Merge branch 'master' of github.com:moxie0/optin", 67 | "modified": [], 68 | "removed": [], 69 | "timestamp": "2013-12-14T11:42:44-08:00", 70 | "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 71 | }, 72 | "pusher": { 73 | "email": "moxie@thoughtcrime.org", 74 | "name": "moxie0" 75 | }, 76 | "ref": "refs/heads/master", 77 | "repository": { 78 | "created_at": 1386866024, 79 | "description": "test", 80 | "fork": false, 81 | "forks": 1, 82 | "has_downloads": true, 83 | "has_issues": true, 84 | "has_wiki": true, 85 | "id": 15141344, 86 | "master_branch": "master", 87 | "name": "tempt", 88 | "open_issues": 0, 89 | "owner": { 90 | "email": "moxie@thoughtcrime.org", 91 | "name": "moxie0" 92 | }, 93 | "private": false, 94 | "pushed_at": 1387050173, 95 | "size": 216, 96 | "stargazers": 1, 97 | "url": "https://github.com/moxie0/optin", 98 | "watchers": 1 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/resources/payloads/non_master_push.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 3 | "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "moxie@thoughtcrime.org", 9 | "name": "Moxie Marlinspike", 10 | "username": "moxie0" 11 | }, 12 | "committer": { 13 | "email": "moxie@thoughtcrime.org", 14 | "name": "Moxie Marlinspike", 15 | "username": "moxie0" 16 | }, 17 | "distinct": true, 18 | "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", 19 | "message": "Update path", 20 | "modified": [ 21 | "README.md" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:42:28-08:00", 25 | "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 41 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 42 | "modified": [], 43 | "removed": [], 44 | "timestamp": "2013-12-14T11:42:44-08:00", 45 | "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 46 | } 47 | ], 48 | "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", 49 | "created": false, 50 | "deleted": false, 51 | "forced": false, 52 | "head_commit": { 53 | "added": [], 54 | "author": { 55 | "email": "moxie@thoughtcrime.org", 56 | "name": "Moxie Marlinspike", 57 | "username": "moxie0" 58 | }, 59 | "committer": { 60 | "email": "moxie@thoughtcrime.org", 61 | "name": "Moxie Marlinspike", 62 | "username": "moxie0" 63 | }, 64 | "distinct": true, 65 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 66 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 67 | "modified": [], 68 | "removed": [], 69 | "timestamp": "2013-12-14T11:42:44-08:00", 70 | "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 71 | }, 72 | "pusher": { 73 | "email": "moxie@thoughtcrime.org", 74 | "name": "moxie0" 75 | }, 76 | "ref": "refs/heads/lilia_doing_something", 77 | "repository": { 78 | "created_at": 1386866024, 79 | "description": "test", 80 | "fork": false, 81 | "forks": 1, 82 | "has_downloads": true, 83 | "has_issues": true, 84 | "has_wiki": true, 85 | "id": 15141344, 86 | "master_branch": "master", 87 | "name": "tempt", 88 | "open_issues": 0, 89 | "owner": { 90 | "email": "moxie@thoughtcrime.org", 91 | "name": "moxie0" 92 | }, 93 | "private": false, 94 | "pushed_at": 1387050173, 95 | "size": 216, 96 | "stargazers": 1, 97 | "url": "https://github.com/moxie0/test", 98 | "watchers": 1 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/resources/payloads/opt_in_commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 3 | "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "moxie@thoughtcrime.org", 9 | "name": "Moxie Marlinspike", 10 | "username": "moxie0" 11 | }, 12 | "committer": { 13 | "email": "moxie@thoughtcrime.org", 14 | "name": "Moxie Marlinspike", 15 | "username": "moxie0" 16 | }, 17 | "distinct": true, 18 | "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", 19 | "message": "Update path MONEYMONEY", 20 | "modified": [ 21 | "README.md" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:42:28-08:00", 25 | "url": "https://github.com/moxie0/optin/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 41 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 42 | "modified": [], 43 | "removed": [], 44 | "timestamp": "2013-12-14T11:42:44-08:00", 45 | "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 46 | } 47 | ], 48 | "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", 49 | "created": false, 50 | "deleted": false, 51 | "forced": false, 52 | "head_commit": { 53 | "added": [], 54 | "author": { 55 | "email": "moxie@thoughtcrime.org", 56 | "name": "Moxie Marlinspike", 57 | "username": "moxie0" 58 | }, 59 | "committer": { 60 | "email": "moxie@thoughtcrime.org", 61 | "name": "Moxie Marlinspike", 62 | "username": "moxie0" 63 | }, 64 | "distinct": true, 65 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 66 | "message": "Merge branch 'master' of github.com:moxie0/optin", 67 | "modified": [], 68 | "removed": [], 69 | "timestamp": "2013-12-14T11:42:44-08:00", 70 | "url": "https://github.com/moxie0/optin/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 71 | }, 72 | "pusher": { 73 | "email": "moxie@thoughtcrime.org", 74 | "name": "moxie0" 75 | }, 76 | "ref": "refs/heads/master", 77 | "repository": { 78 | "created_at": 1386866024, 79 | "description": "test", 80 | "fork": false, 81 | "forks": 1, 82 | "has_downloads": true, 83 | "has_issues": true, 84 | "has_wiki": true, 85 | "id": 15141344, 86 | "master_branch": "master", 87 | "name": "tempt", 88 | "open_issues": 0, 89 | "owner": { 90 | "email": "moxie@thoughtcrime.org", 91 | "name": "moxie0" 92 | }, 93 | "private": false, 94 | "pushed_at": 1387050173, 95 | "size": 216, 96 | "stargazers": 1, 97 | "url": "https://github.com/moxie0/optin", 98 | "watchers": 1 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/resources/payloads/opt_out_commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 3 | "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "moxie@thoughtcrime.org", 9 | "name": "Moxie Marlinspike", 10 | "username": "moxie0" 11 | }, 12 | "committer": { 13 | "email": "moxie@thoughtcrime.org", 14 | "name": "Moxie Marlinspike", 15 | "username": "moxie0" 16 | }, 17 | "distinct": true, 18 | "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", 19 | "message": "Update path FREEBIE", 20 | "modified": [ 21 | "README.md" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:42:28-08:00", 25 | "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 41 | "message": "Merge branch 'master' of github.com:moxie0/tempt FREEBIE", 42 | "modified": [], 43 | "removed": [], 44 | "timestamp": "2013-12-14T11:42:44-08:00", 45 | "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 46 | } 47 | ], 48 | "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", 49 | "created": false, 50 | "deleted": false, 51 | "forced": false, 52 | "head_commit": { 53 | "added": [], 54 | "author": { 55 | "email": "moxie@thoughtcrime.org", 56 | "name": "Moxie Marlinspike", 57 | "username": "moxie0" 58 | }, 59 | "committer": { 60 | "email": "moxie@thoughtcrime.org", 61 | "name": "Moxie Marlinspike", 62 | "username": "moxie0" 63 | }, 64 | "distinct": true, 65 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 66 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 67 | "modified": [], 68 | "removed": [], 69 | "timestamp": "2013-12-14T11:42:44-08:00", 70 | "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 71 | }, 72 | "pusher": { 73 | "email": "moxie@thoughtcrime.org", 74 | "name": "moxie0" 75 | }, 76 | "ref": "refs/heads/master", 77 | "repository": { 78 | "created_at": 1386866024, 79 | "description": "test", 80 | "fork": false, 81 | "forks": 1, 82 | "has_downloads": true, 83 | "has_issues": true, 84 | "has_wiki": true, 85 | "id": 15141344, 86 | "master_branch": "master", 87 | "name": "tempt", 88 | "open_issues": 0, 89 | "owner": { 90 | "email": "moxie@thoughtcrime.org", 91 | "name": "moxie0" 92 | }, 93 | "private": false, 94 | "pushed_at": 1387050173, 95 | "size": 216, 96 | "stargazers": 1, 97 | "url": "https://github.com/moxie0/test", 98 | "watchers": 1 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/resources/payloads/transactions.json: -------------------------------------------------------------------------------- 1 | { 2 | "current_user": { 3 | "id": "5011f33df8182b142400000e", 4 | "email": "user2@example.com", 5 | "name": "User Two" 6 | }, 7 | "balance": { 8 | "amount": "50.00000000", 9 | "currency": "BTC" 10 | }, 11 | "total_count": 2, 12 | "num_pages": 1, 13 | "current_page": 1, 14 | "transactions": [ 15 | { 16 | "transaction": { 17 | "id": "5018f833f8182b129c00002f", 18 | "created_at": "2012-08-01T02:34:43-07:00", 19 | "amount": { 20 | "amount": "-1.10000000", 21 | "currency": "BTC" 22 | }, 23 | "request": true, 24 | "status": "pending", 25 | "sender": { 26 | "id": "5011f33df8182b142400000e", 27 | "name": "User Two", 28 | "email": "user2@example.com" 29 | }, 30 | "recipient": { 31 | "id": "5011f33df8182b142400000a", 32 | "name": "User One", 33 | "email": "user1@example.com" 34 | }, 35 | "notes": "Commit payment:__moxie0__ https://github.com/WhisperSystems/BitHub/commit/88edf54e5b57c80ac05093a9be90965fd41291c2" 36 | } 37 | }, 38 | { 39 | "transaction": { 40 | "id": "5018f833f8182b129c00002e", 41 | "created_at": "2012-08-01T02:36:43-07:00", 42 | "hsh": "9d6a7d1112c3db9de5315b421a5153d71413f5f752aff75bf504b77df4e646a3", 43 | "amount": { 44 | "amount": "-1.00000000", 45 | "currency": "BTC" 46 | }, 47 | "request": false, 48 | "status": "complete", 49 | "sender": { 50 | "id": "5011f33df8182b142400000e", 51 | "name": "User Two", 52 | "email": "user2@example.com" 53 | }, 54 | "recipient_address": "37muSN5ZrukVTvyVh3mT5Zc5ew9L9CBare", 55 | "notes": "Commit payment:__moxie0__ https://github.com/WhisperSystems/BitHub/commit/88edf54e5b57c80ac05093a9be90965fd41291c2" 56 | } 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /src/test/resources/payloads/valid_commit.json: -------------------------------------------------------------------------------- 1 | { 2 | "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 3 | "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", 4 | "commits": [ 5 | { 6 | "added": [], 7 | "author": { 8 | "email": "moxie@thoughtcrime.org", 9 | "name": "Moxie Marlinspike", 10 | "username": "moxie0" 11 | }, 12 | "committer": { 13 | "email": "moxie@thoughtcrime.org", 14 | "name": "Moxie Marlinspike", 15 | "username": "moxie0" 16 | }, 17 | "distinct": true, 18 | "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", 19 | "message": "Update path", 20 | "modified": [ 21 | "README.md" 22 | ], 23 | "removed": [], 24 | "timestamp": "2013-12-14T11:42:28-08:00", 25 | "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" 26 | }, 27 | { 28 | "added": [], 29 | "author": { 30 | "email": "moxie@thoughtcrime.org", 31 | "name": "Moxie Marlinspike", 32 | "username": "moxie0" 33 | }, 34 | "committer": { 35 | "email": "moxie@thoughtcrime.org", 36 | "name": "Moxie Marlinspike", 37 | "username": "moxie0" 38 | }, 39 | "distinct": true, 40 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 41 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 42 | "modified": [], 43 | "removed": [], 44 | "timestamp": "2013-12-14T11:42:44-08:00", 45 | "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 46 | } 47 | ], 48 | "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", 49 | "created": false, 50 | "deleted": false, 51 | "forced": false, 52 | "head_commit": { 53 | "added": [], 54 | "author": { 55 | "email": "moxie@thoughtcrime.org", 56 | "name": "Moxie Marlinspike", 57 | "username": "moxie0" 58 | }, 59 | "committer": { 60 | "email": "moxie@thoughtcrime.org", 61 | "name": "Moxie Marlinspike", 62 | "username": "moxie0" 63 | }, 64 | "distinct": true, 65 | "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", 66 | "message": "Merge branch 'master' of github.com:moxie0/tempt", 67 | "modified": [], 68 | "removed": [], 69 | "timestamp": "2013-12-14T11:42:44-08:00", 70 | "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" 71 | }, 72 | "pusher": { 73 | "email": "moxie@thoughtcrime.org", 74 | "name": "moxie0" 75 | }, 76 | "ref": "refs/heads/master", 77 | "repository": { 78 | "created_at": 1386866024, 79 | "description": "test", 80 | "fork": false, 81 | "forks": 1, 82 | "has_downloads": true, 83 | "has_issues": true, 84 | "has_wiki": true, 85 | "id": 15141344, 86 | "master_branch": "master", 87 | "name": "tempt", 88 | "open_issues": 0, 89 | "owner": { 90 | "email": "moxie@thoughtcrime.org", 91 | "name": "moxie0" 92 | }, 93 | "private": false, 94 | "pushed_at": 1387050173, 95 | "size": 216, 96 | "stargazers": 1, 97 | "url": "https://github.com/moxie0/test", 98 | "watchers": 1 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.7 2 | --------------------------------------------------------------------------------
{{organizationName}}
Donate BTC today
{{description}}