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