Initial Commit

This commit is contained in:
Moxie Marlinspike 2013-12-16 10:06:10 -08:00
commit 90c8512439
48 changed files with 2338 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea
*.iml
target
config/production.yml

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: java $JAVA_OPTS -Ddw.http.port=$PORT -Ddw.http.adminPort=$PORT -Ddw.github.user=$GITHUB_USER -Ddw.github.token=$GITHUB_TOKEN -Ddw.github.repositories_heroku=$GITHUB_REPOSITORIES -Ddw.coinbase.apiKey=$COINBASE_API_KEY -jar target/BitHub-0.1.jar server

45
README.md Normal file
View File

@ -0,0 +1,45 @@
BitHub
=================
BitHub is a service that will automatically pay a percentage of Bitcoin funds for every submission to a GitHub repository.
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
------------
1. `$ heroku create your_app_name`
1. `$ heroku config:set GITHUB_USER=your_bithub_username`
1. `$ heroku config:set GITHUB_TOKEN=your_bithub_authtoken`
1. `$ heroku config:set GITHUB_REPOSITORIES="[\"https://github.com/youraccount/yourrepo\", \"https://github.com/youraccount/yourotherrepo\"]"`
1. `$ heroku config:set COINBASE_API_KEY=your_api_key`
1. `$ git remote add your_heroku_remote`
1. `$ git push heroku master`
Mailing list
------------
Have a question? Ask on our mailing list!
whispersystems@lists.riseup.net
https://lists.riseup.net/www/info/whispersystems
Current BitHub Payment For Commit:
=================
![Current Price](https://bithub.herokuapp.com/v1/status/payment/commit)

25
assembly.xml Normal file
View File

@ -0,0 +1,25 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>bin</id>
<includeBaseDirectory>false</includeBaseDirectory>
<formats>
<format>tar.gz</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}/config</directory>
<outputDirectory>/config</outputDirectory>
<includes>
<include>*</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>${project.name}-${project.version}.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

8
config/sample.yml Normal file
View File

@ -0,0 +1,8 @@
github:
user: # Your BitHub instance's GitHub username.
token: # Your BitHub instance's GitHub auth token.
repositories:
- # A list of repository URLs to support payouts for.
coinbase:
apiKey: # Your Coinbase API key.

BIN
design/badge-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

BIN
design/badge-small.xcf Normal file

Binary file not shown.

BIN
design/badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
design/badge.xcf Normal file

Binary file not shown.

175
pom.xml Normal file
View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<prerequisites>
<maven>3.0.0</maven>
</prerequisites>
<groupId>org.whispersystems.bithub</groupId>
<artifactId>BitHub</artifactId>
<version>0.1</version>
<dependencies>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-jdbi</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-client</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-migrations</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.17.1</version>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-multipart</artifactId>
<version>1.17.1</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.2</version>
</dependency>
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-views</artifactId>
<version>0.6.2</version>
</dependency>
<dependency>
<groupId>org.ocpsoft.prettytime</groupId>
<artifactId>prettytime</artifactId>
<version>2.1.3.Final</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.6</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.whispersystems.bithub.BithubService</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,58 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yammer.dropwizard.config.Configuration;
import org.whispersystems.bithub.config.BithubConfiguration;
import org.whispersystems.bithub.config.CoinbaseConfiguration;
import org.whispersystems.bithub.config.GithubConfiguration;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class BithubServerConfiguration extends Configuration {
@Valid
@NotNull
@JsonProperty
private GithubConfiguration github;
@Valid
@NotNull
@JsonProperty
private CoinbaseConfiguration coinbase;
@JsonProperty
@Valid
private BithubConfiguration bithub = new BithubConfiguration();
public GithubConfiguration getGithubConfiguration() {
return github;
}
public CoinbaseConfiguration getCoinbaseConfiguration() {
return coinbase;
}
public BithubConfiguration getBithubConfiguration() {
return bithub;
}
}

View File

@ -0,0 +1,70 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub;
import com.yammer.dropwizard.Service;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.config.Environment;
import com.yammer.dropwizard.views.ViewBundle;
import org.whispersystems.bithub.client.CoinbaseClient;
import org.whispersystems.bithub.client.GithubClient;
import org.whispersystems.bithub.controllers.GithubController;
import org.whispersystems.bithub.controllers.StatusController;
import org.whispersystems.bithub.filters.CorsHeaderFilter;
import org.whispersystems.bithub.mappers.IOExceptionMapper;
import org.whispersystems.bithub.mappers.UnauthorizedHookExceptionMapper;
import java.math.BigDecimal;
import java.util.List;
/**
* The main entry point for the service.
*
* @author Moxie Marlinspike
*/
public class BithubService extends Service<BithubServerConfiguration> {
@Override
public void initialize(Bootstrap<BithubServerConfiguration> bootstrap) {
bootstrap.setName("bithub-server");
bootstrap.addBundle(new ViewBundle());
}
@Override
public void run(BithubServerConfiguration config, Environment environment)
throws Exception
{
String githubUser = config.getGithubConfiguration().getUser();
String githubToken = config.getGithubConfiguration().getToken();
List<String> githubRepositories = config.getGithubConfiguration().getRepositories();
BigDecimal payoutRate = config.getBithubConfiguration().getPayoutRate();
GithubClient githubClient = new GithubClient(githubUser, githubToken);
CoinbaseClient coinbaseClient = new CoinbaseClient(config.getCoinbaseConfiguration().getApiKey());
environment.addFilter(new CorsHeaderFilter(), "/v1/status/*");
environment.addResource(new GithubController(githubRepositories, githubClient, coinbaseClient, payoutRate));
environment.addResource(new StatusController(coinbaseClient, payoutRate));
environment.addProvider(new IOExceptionMapper());
environment.addProvider(new UnauthorizedHookExceptionMapper());
}
public static void main(String[] args) throws Exception {
new BithubService().run(args);
}
}

View File

@ -0,0 +1,144 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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.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 org.whispersystems.bithub.entities.Author;
import org.whispersystems.bithub.entities.BalanceResponse;
import org.whispersystems.bithub.entities.BitcoinTransaction;
import org.whispersystems.bithub.entities.BitcoinTransactionResponse;
import org.whispersystems.bithub.entities.ExchangeRate;
import org.whispersystems.bithub.entities.RecentTransactionsResponse;
import org.whispersystems.bithub.entities.Transaction;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
/**
* Handles interaction with the Coinbase API.
*
* @author Moxie Marlinspike
*/
public class CoinbaseClient {
private static final String COINBASE_URL = "https://coinbase.com/";
private static final String BALANCE_PATH = "/api/v1/account/balance";
private static final String PAYMENT_PATH = "/api/v1/transactions/send_money";
private static final String EXCHANGE_PATH = "/api/v1/currencies/exchange_rates";
private static final String RECENT_TRANSACTIONS_PATH = "/api/v1/transactions";
private final String apiKey;
private final Client client;
public CoinbaseClient(String apiKey) {
this.apiKey = apiKey;
this.client = Client.create(getClientConfig());
}
public List<Transaction> getRecentTransactions() throws IOException {
try {
return client.resource(COINBASE_URL)
.path(RECENT_TRANSACTIONS_PATH)
.queryParam("api_key", apiKey)
.get(RecentTransactionsResponse.class).getTransactions();
} catch (UniformInterfaceException | ClientHandlerException e) {
throw new IOException(e);
}
}
public BigDecimal getExchangeRate() throws IOException {
try {
WebResource resource = client.resource(COINBASE_URL)
.path(EXCHANGE_PATH);
String btcToUsd = resource.accept(MediaType.APPLICATION_JSON)
.get(ExchangeRate.class)
.getBtc_to_usd();
return new BigDecimal(btcToUsd);
} catch (UniformInterfaceException | ClientHandlerException e) {
throw new IOException(e);
}
}
public void sendPayment(Author author, BigDecimal amount, String url)
throws TransferFailedException
{
try {
WebResource resource = client.resource(COINBASE_URL)
.path(PAYMENT_PATH)
.queryParam("api_key", apiKey);
String note = "Commit payment:\n__" + author.getUsername() + "__ " + url;
BitcoinTransaction transaction = new BitcoinTransaction(author.getEmail(),
amount.toPlainString(),
note);
boolean success = resource.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.entity(transaction)
.post(BitcoinTransactionResponse.class)
.isSuccess();
if (!success) {
throw new TransferFailedException();
}
} catch (UniformInterfaceException | ClientHandlerException e) {
throw new TransferFailedException(e);
}
}
public BigDecimal getAccountBalance() throws IOException {
try {
WebResource resource = client.resource(COINBASE_URL)
.path(BALANCE_PATH)
.queryParam("api_key", apiKey);
String amount = resource.accept(MediaType.APPLICATION_JSON)
.get(BalanceResponse.class)
.getAmount();
if (amount == null) {
throw new IOException("Empty amount in response!");
}
return new BigDecimal(amount);
} catch (UniformInterfaceException | ClientHandlerException e) {
throw new IOException(e);
}
}
private ClientConfig getClientConfig() {
ClientConfig config = new DefaultClientConfig();
config.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
return config;
}
}

View File

@ -0,0 +1,90 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 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 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));
}
}

View File

@ -0,0 +1,29 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.client;
public class TransferFailedException extends Throwable {
public TransferFailedException() {
super();
}
public TransferFailedException(RuntimeException e) {
super(e);
}
}

View File

@ -0,0 +1,37 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -0,0 +1,33 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
public String getApiKey() {
return apiKey;
}
}

View File

@ -0,0 +1,73 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 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<String> repositories;
@JsonProperty
private String repositories_heroku;
public String getUser() {
return user;
}
public String getToken() {
return token;
}
public List<String> getRepositories() {
if (repositories != null) {
return repositories;
}
if (repositories_heroku != null) {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(repositories_heroku, new TypeReference<List<String>>() {});
} catch (IOException e) {
logger.warn("Error deserializing", e);
}
}
return new LinkedList<>();
}
}

View File

@ -0,0 +1,177 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yammer.metrics.annotation.Timed;
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.client.CoinbaseClient;
import org.whispersystems.bithub.client.GithubClient;
import org.whispersystems.bithub.client.TransferFailedException;
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.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* 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 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 Set<String> repositories;
private final BigDecimal payoutRate;
public GithubController(List<String> repositories,
GithubClient githubClient,
CoinbaseClient coinbaseClient,
BigDecimal payoutRate)
{
this.coinbaseClient = coinbaseClient;
this.githubClient = githubClient;
this.repositories = new HashSet<>();
this.payoutRate = payoutRate;
for (String repository : repositories) {
this.repositories.add(repository.toLowerCase());
}
}
@Timed
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Path("/commits/")
public void handleCommits(@HeaderParam("X-Forwarded-For") String clientIp,
@FormParam("payload") String eventString)
throws IOException, UnauthorizedHookException
{
authenticate(clientIp);
PushEvent event = getEventFromPayload(eventString);
if (!repositories.contains(event.getRepository().getUrl().toLowerCase())) {
throw new UnauthorizedHookException("Not a valid repository: " + event.getRepository().getUrl());
}
Repository repository = event.getRepository();
List<Commit> commits = getQualifyingCommits(event);
BigDecimal balance = coinbaseClient.getAccountBalance();
logger.info("Retrieved balance: " + balance.toPlainString());
sendPaymentsFor(repository, commits, balance);
}
private void sendPaymentsFor(Repository repository, List<Commit> commits, BigDecimal balance) {
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));
} 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<Commit> getQualifyingCommits(PushEvent event) {
List<Commit> commits = new LinkedList<>();
Set<String> 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 (commit.getMessage() == null || !commit.getMessage().startsWith("Merge")) {
logger.info("Not a merge commit...");
emails.add(commit.getAuthor().getEmail());
commits.add(commit);
}
}
}
return commits;
}
private boolean isViablePaymentAmount(BigDecimal payment) {
return payment.compareTo(new BigDecimal(0)) == 1;
}
private String getCommitCommentStringForPayment(BigDecimal payment) {
if (isViablePaymentAmount(payment)) {
payment = payment.setScale(2, RoundingMode.CEILING);
return "Thanks! BitHub has sent payment of $" + payment.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);
}
}
}

View File

@ -0,0 +1,170 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.controllers;
import com.yammer.metrics.annotation.Timed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.bithub.client.CoinbaseClient;
import org.whispersystems.bithub.entities.Payment;
import org.whispersystems.bithub.entities.Transaction;
import org.whispersystems.bithub.util.Badge;
import org.whispersystems.bithub.views.RecentTransactionsView;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
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;
/**
* Handles incoming API calls for BitHub instance status information.
*
* @author Moxie Marlinspike
*/
@Path("/v1/status")
public class StatusController {
private static final int UPDATE_FREQUENCY_MILLIS = 60 * 1000;
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
private final Logger logger = LoggerFactory.getLogger(StatusController.class);
private final AtomicReference<CurrentPayment> cachedPaymentStatus;
private final AtomicReference<RecentTransactionsView> cachedTransactions;
private final BigDecimal payoutRate;
public StatusController(CoinbaseClient coinbaseClient, BigDecimal payoutRate) throws IOException {
this.payoutRate = payoutRate;
this.cachedPaymentStatus = new AtomicReference<>(createCurrentPaymentForBalance(coinbaseClient));
this.cachedTransactions = new AtomicReference<>(createRecentTransactionsView(coinbaseClient));
initializeUpdates(coinbaseClient);
}
@Timed
@GET
@Path("/transactions")
public Response getTransactions(@QueryParam("format") @DefaultValue("html") String format)
throws IOException
{
if (format.equals("json")) {
return Response.ok(cachedTransactions.get(), MediaType.APPLICATION_JSON_TYPE).build();
} else {
return Response.ok(cachedTransactions.get(), MediaType.TEXT_HTML_TYPE).build();
}
}
@Timed
@GET
@Path("/payment/commit")
public Response getCurrentCommitPrice(@QueryParam("format") @DefaultValue("png") String format)
throws IOException
{
if (format.equals("json")) {
return Response.ok(cachedPaymentStatus.get().getEntity(), MediaType.APPLICATION_JSON_TYPE).build();
} else if (format.equals("png_small")) {
return Response.ok(cachedPaymentStatus.get().getSmallBadge(), "image/png").build();
} else {
return Response.ok(cachedPaymentStatus.get().getBadge(), "image/png").build();
}
}
private CurrentPayment createCurrentPaymentForBalance(CoinbaseClient coinbaseClient)
throws IOException
{
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 RecentTransactionsView createRecentTransactionsView(CoinbaseClient coinbaseClient)
throws IOException
{
List<Transaction> recentTransactions = coinbaseClient.getRecentTransactions();
BigDecimal exchangeRate = coinbaseClient.getExchangeRate();
return new RecentTransactionsView(recentTransactions, exchangeRate);
}
public void initializeUpdates(final CoinbaseClient coinbaseClient) {
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
CurrentPayment currentPayment = createCurrentPaymentForBalance(coinbaseClient);
cachedPaymentStatus.set(currentPayment);
} catch (IOException e) {
logger.warn("Failed to update badge", e);
}
}
}, UPDATE_FREQUENCY_MILLIS, UPDATE_FREQUENCY_MILLIS, TimeUnit.MILLISECONDS);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
RecentTransactionsView view = createRecentTransactionsView(coinbaseClient);
cachedTransactions.set(view);
} catch (IOException e) {
logger.warn("Failed to update recent transactions", e);
}
}
}, UPDATE_FREQUENCY_MILLIS, UPDATE_FREQUENCY_MILLIS, TimeUnit.MILLISECONDS);
}
private class CurrentPayment {
private final byte[] badge;
private final byte[] smallBadge;
private final Payment entity;
private CurrentPayment(byte[] badge, byte[] smallBadge, Payment entity) {
this.badge = badge;
this.smallBadge = smallBadge;
this.entity = entity;
}
private byte[] getBadge() {
return badge;
}
private byte[] getSmallBadge() {
return smallBadge;
}
private Payment getEntity() {
return entity;
}
}
}

View File

@ -0,0 +1,24 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.controllers;
public class UnauthorizedHookException extends Throwable {
public UnauthorizedHookException(String s) {
super(s);
}
}

View File

@ -0,0 +1,34 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 Amount {
@JsonProperty
@NotEmpty
private String amount;
public String getAmount() {
return amount;
}
}

View File

@ -0,0 +1,48 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,34 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 BalanceResponse {
@JsonProperty
@NotEmpty
private String amount;
public String getAmount() {
return amount;
}
}

View File

@ -0,0 +1,38 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
public class BitcoinTransaction {
@JsonProperty
private String to;
@JsonProperty
private String amount;
@JsonProperty
private String notes;
public BitcoinTransaction(String to, String amount, String notes) {
this.to = to;
this.amount = amount;
this.notes = notes;
}
}

View File

@ -0,0 +1,30 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.entities;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class BitcoinTransactionResponse {
private boolean success;
public boolean isSuccess() {
return success;
}
}

View File

@ -0,0 +1,63 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,31 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,34 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 ExchangeRate {
@NotEmpty
@JsonProperty
private String btc_to_usd;
public String getBtc_to_usd() {
return btc_to_usd;
}
}

View File

@ -0,0 +1,29 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -0,0 +1,66 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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<Commit> 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<Commit> getCommits() {
return commits;
}
}

View File

@ -0,0 +1,43 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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.LinkedList;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class RecentTransactionsResponse {
@JsonProperty
@NotNull
private List<TransactionWrapper> transactions;
public List<Transaction> getTransactions() {
List<Transaction> rawTransactions = new LinkedList<Transaction>();
for (TransactionWrapper transactionWrapper : transactions) {
rawTransactions.add(transactionWrapper.getTransaction());
}
return rawTransactions;
}
}

View File

@ -0,0 +1,52 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
public Author getOwner() {
return owner;
}
public String getName() {
return name;
}
public String getUrl() {
return url;
}
}

View File

@ -0,0 +1,58 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 Transaction {
@JsonProperty(value = "created_at")
@NotEmpty
private String createdTime;
@JsonProperty
@NotNull
private Amount amount;
@JsonProperty(value = "recipient_address")
private String recipientAddress;
@JsonProperty
private String notes;
public String getCreatedTime() {
return createdTime;
}
public String getAmount() {
return amount.getAmount();
}
public String getRecipientAddress() {
return recipientAddress;
}
public String getNotes() {
return notes;
}
}

View File

@ -0,0 +1,34 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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 TransactionWrapper {
@JsonProperty
@NotNull
private Transaction transaction;
public Transaction getTransaction() {
return transaction;
}
}

View File

@ -0,0 +1,40 @@
package org.whispersystems.bithub.filters;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CorsHeaderFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (response instanceof HttpServletResponse) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
httpServletResponse.addHeader("Access-Control-Allow-Origin", "*");
if ("OPTIONS".equals(httpServletRequest.getMethod())) {
httpServletResponse.addHeader("Access-Control-Allow-Headers", "Content-Type");
}
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}

View File

@ -0,0 +1,38 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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<IOException> {
private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class);
@Override
public Response toResponse(IOException e) {
logger.warn("IOExceptionMapper", e);
return Response.status(503).build();
}
}

View File

@ -0,0 +1,38 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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<UnauthorizedHookException> {
private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class);
@Override
public Response toResponse(UnauthorizedHookException e) {
logger.warn("IOExceptionMapper", e);
return Response.status(401).build();
}
}

View File

@ -0,0 +1,41 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}
}

View File

@ -0,0 +1,65 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -0,0 +1,65 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.views;
import com.yammer.dropwizard.views.View;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.bithub.entities.Transaction;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
/**
* A rendered HTML view of recent BitHub transactions.
*
* @author Moxie Marlinspike
*/
public class RecentTransactionsView extends View {
private final Logger logger = LoggerFactory.getLogger(RecentTransactionsView.class);
private final List<TransactionView> transactions = new LinkedList<>();
public RecentTransactionsView(List<Transaction> recentTransactions, BigDecimal exchangeRate) {
super("recent_transactions.mustache");
for (Transaction transaction : recentTransactions) {
try {
if (isSentTransaction(transaction)) {
transactions.add(new TransactionView(exchangeRate,
transaction.getAmount(),
transaction.getCreatedTime(),
transaction.getNotes()));
}
} catch (ParseException e) {
logger.warn("Error parsing: ", e);
}
}
}
private boolean isSentTransaction(Transaction transaction) {
BigDecimal amount = new BigDecimal(transaction.getAmount());
return amount.compareTo(new BigDecimal(0.0)) < 0;
}
public List<TransactionView> getTransactions() {
return transactions;
}
}

View File

@ -0,0 +1,129 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.views;
import org.apache.commons.lang3.StringEscapeUtils;
import org.ocpsoft.prettytime.PrettyTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* A rendered HTML view of an individual BitHub transaction.
*
* @author Moxie Marlinspike
*/
public class TransactionView {
private final Logger logger = LoggerFactory.getLogger(RecentTransactionsView.class);
private final String destination;
private final String amount;
private final String commitUrl;
private final String commitSha;
private final String timestamp;
public TransactionView(BigDecimal exchangeRate, String amount,
String timestamp, String message)
throws ParseException
{
this.amount = getAmountInDollars(exchangeRate, amount);
this.destination = parseDestinationFromMessage(message);
this.timestamp = parseTimestamp(timestamp);
this.commitUrl = parseUrlFromMessage(message);
this.commitSha = parseShaFromUrl(commitUrl);
}
private String getAmountInDollars(BigDecimal exchangeRate, String amount) {
return new BigDecimal(amount).abs()
.multiply(exchangeRate)
.setScale(2, RoundingMode.CEILING)
.toPlainString();
}
private String parseTimestamp(String timestamp) throws ParseException {
int offendingColon = timestamp.lastIndexOf(':');
String fixedTimestamp = timestamp.substring(0, offendingColon) + timestamp.substring(offendingColon + 1);
return new PrettyTime().format(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(fixedTimestamp));
}
private String parseDestinationFromMessage(String message) {
message = StringEscapeUtils.unescapeHtml4(message);
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);
}
private String parseUrlFromMessage(String message) throws ParseException {
message = StringEscapeUtils.unescapeHtml4(message);
int urlIndex = message.indexOf("https://");
return message.substring(urlIndex).trim();
}
private 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);
}
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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,10 @@
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"

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="script-src none">
<meta charset="utf-8">
<link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<style>
body {
font-family: 'Open Sans', sans-serif;
letter-spacing: .01rem;
font-weight: 400;
font-style: normal;
font-size: 22px;
line-height: 1.5;
color: #333332;
}
sha {
font-family: Consolas, "Liberation Mono", Courier, monospace;
color: #4183c4;
margin: 2px;
padding: 3px;
border: 1px solid #ddd;
background-color: #f8f8f8;
border-radius: 3px;
font-size: 20px;
}
ul { list-style-type: none;}
li { padding: 5px; padding-left: 20px;}
li:nth-child(even) { background-color: #ffffff; }
li:nth-child(odd) { background-color: #eeeeee;}
a { text-decoration: none; color: #555; }
</style>
</head>
<body>
<ul>
{{#transactions}}
<li>Sent ${{amount}} USD to <a href="https://github.com/{{destination}}">{{destination}}</a> for <a href="{{commitUrl}}"><sha>{{commitSha}}</sha></a> {{timestamp}}.</li>
{{/transactions}}
</ul>
</body>
</html>

View File

@ -0,0 +1,101 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.bithub.tests.controllers;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import com.yammer.dropwizard.testing.ResourceTest;
import org.junit.Test;
import org.whispersystems.bithub.client.CoinbaseClient;
import org.whispersystems.bithub.client.GithubClient;
import org.whispersystems.bithub.client.TransferFailedException;
import org.whispersystems.bithub.controllers.GithubController;
import org.whispersystems.bithub.entities.Author;
import org.whispersystems.bithub.mappers.UnauthorizedHookExceptionMapper;
import javax.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.List;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class GithubControllerTest extends ResourceTest {
private static final BigDecimal BALANCE = new BigDecimal(10.01);
private final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class);
private final GithubClient githubClient = mock(GithubClient.class);
private final List<String> repositories = new LinkedList<String>() {{
add("https://github.com/moxie0/test");
}};
@Override
protected void setUpResources() throws Exception {
when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE);
addResource(new GithubController(repositories, githubClient, coinbaseClient, new BigDecimal(0.02)));
addProvider(new UnauthorizedHookExceptionMapper());
}
@Test
public void testInvalidRepository() throws Exception {
String payloadValue = "{\"ref\":\"refs/heads/master\",\"after\":\"100e9859651b35a3505cc278e9a98a076f79940b\",\"before\":\"6626766348ab245bdb3351989f753bd6e792524a\",\"created\":false,\"deleted\":false,\"forced\":false,\"compare\":\"https://github.com/moxie0/tempt/compare/6626766348ab...100e9859651b\",\"commits\":[{\"id\":\"fd7daeb1de6d72220b1313a7f1112d43885013aa\",\"distinct\":true,\"message\":\"Update foo\",\"timestamp\":\"2013-12-14T11:27:00-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/fd7daeb1de6d72220b1313a7f1112d43885013aa\",\"author\":{\"name\":\"WhisperBTC\",\"email\":\"info@whispersystems.org\",\"username\":\"WhisperBTC\"},\"committer\":{\"name\":\"WhisperBTC\",\"email\":\"info@whispersystems.org\",\"username\":\"WhisperBTC\"},\"added\":[],\"removed\":[],\"modified\":[\"foo\"]},{\"id\":\"100e9859651b35a3505cc278e9a98a076f79940b\",\"distinct\":true,\"message\":\"Merge pull request #2 from WhisperBTC/patch-2\\n\\nUpdate foo\",\"timestamp\":\"2013-12-14T11:27:28-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[\"foo\"]}],\"head_commit\":{\"id\":\"100e9859651b35a3505cc278e9a98a076f79940b\",\"distinct\":true,\"message\":\"Merge pull request #2 from WhisperBTC/patch-2\\n\\nUpdate foo\",\"timestamp\":\"2013-12-14T11:27:28-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[\"foo\"]},\"repository\":{\"id\":15141344,\"name\":\"tempt\",\"url\":\"https://github.com/moxie0/tempt\",\"description\":\"test\",\"watchers\":1,\"stargazers\":1,\"forks\":1,\"fork\":false,\"size\":216,\"owner\":{\"name\":\"moxie0\",\"email\":\"moxie@thoughtcrime.org\"},\"private\":false,\"open_issues\":0,\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"created_at\":1386866024,\"pushed_at\":1387049248,\"master_branch\":\"master\"},\"pusher\":{\"name\":\"moxie0\",\"email\":\"moxie@thoughtcrime.org\"}}";
MultivaluedMapImpl post = new MultivaluedMapImpl();
post.add("payload", payloadValue);
ClientResponse response = client().resource("/v1/github/commits/")
.header("X-Forwarded-For", "192.30.252.1")
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
.post(ClientResponse.class, post);
assertThat(response.getStatus()).isEqualTo(401);
}
@Test
public void testInvalidOrigin() throws Exception {
String payloadValue = "{\"ref\":\"refs/heads/master\",\"after\":\"100e9859651b35a3505cc278e9a98a076f79940b\",\"before\":\"6626766348ab245bdb3351989f753bd6e792524a\",\"created\":false,\"deleted\":false,\"forced\":false,\"compare\":\"https://github.com/moxie0/tempt/compare/6626766348ab...100e9859651b\",\"commits\":[{\"id\":\"fd7daeb1de6d72220b1313a7f1112d43885013aa\",\"distinct\":true,\"message\":\"Update foo\",\"timestamp\":\"2013-12-14T11:27:00-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/fd7daeb1de6d72220b1313a7f1112d43885013aa\",\"author\":{\"name\":\"WhisperBTC\",\"email\":\"info@whispersystems.org\",\"username\":\"WhisperBTC\"},\"committer\":{\"name\":\"WhisperBTC\",\"email\":\"info@whispersystems.org\",\"username\":\"WhisperBTC\"},\"added\":[],\"removed\":[],\"modified\":[\"foo\"]},{\"id\":\"100e9859651b35a3505cc278e9a98a076f79940b\",\"distinct\":true,\"message\":\"Merge pull request #2 from WhisperBTC/patch-2\\n\\nUpdate foo\",\"timestamp\":\"2013-12-14T11:27:28-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[\"foo\"]}],\"head_commit\":{\"id\":\"100e9859651b35a3505cc278e9a98a076f79940b\",\"distinct\":true,\"message\":\"Merge pull request #2 from WhisperBTC/patch-2\\n\\nUpdate foo\",\"timestamp\":\"2013-12-14T11:27:28-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/100e9859651b35a3505cc278e9a98a076f79940b\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[\"foo\"]},\"repository\":{\"id\":15141344,\"name\":\"tempt\",\"url\":\"https://github.com/moxie0/test\",\"description\":\"test\",\"watchers\":1,\"stargazers\":1,\"forks\":1,\"fork\":false,\"size\":216,\"owner\":{\"name\":\"moxie0\",\"email\":\"moxie@thoughtcrime.org\"},\"private\":false,\"open_issues\":0,\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"created_at\":1386866024,\"pushed_at\":1387049248,\"master_branch\":\"master\"},\"pusher\":{\"name\":\"moxie0\",\"email\":\"moxie@thoughtcrime.org\"}}";
MultivaluedMapImpl post = new MultivaluedMapImpl();
post.add("payload", payloadValue);
ClientResponse response = client().resource("/v1/github/commits/")
.header("X-Forwarded-For", "192.30.242.1")
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
.post(ClientResponse.class, post);
assertThat(response.getStatus()).isEqualTo(401);
}
@Test
public void testValidCommit() throws Exception, TransferFailedException {
String payloadValue = "{\"ref\":\"refs/heads/master\",\"after\":\"bcf09f8b4a32921114587e4814a3f0849aa9900f\",\"before\":\"1b141aa068165dd1ed376f483cd5fdc2c64f32b1\",\"created\":false,\"deleted\":false,\"forced\":false,\"compare\":\"https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32\",\"commits\":[{\"id\":\"ba1b681c71db4fcd461954b1bf344bc6e29411e5\",\"distinct\":true,\"message\":\"Update path\",\"timestamp\":\"2013-12-14T11:42:28-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[\"README.md\"]},{\"id\":\"bcf09f8b4a32921114587e4814a3f0849aa9900f\",\"distinct\":true,\"message\":\"Merge branch 'master' of github.com:moxie0/tempt\",\"timestamp\":\"2013-12-14T11:42:44-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[]}],\"head_commit\":{\"id\":\"bcf09f8b4a32921114587e4814a3f0849aa9900f\",\"distinct\":true,\"message\":\"Merge branch 'master' of github.com:moxie0/tempt\",\"timestamp\":\"2013-12-14T11:42:44-08:00\",\"url\":\"https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f\",\"author\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"committer\":{\"name\":\"Moxie Marlinspike\",\"email\":\"moxie@thoughtcrime.org\",\"username\":\"moxie0\"},\"added\":[],\"removed\":[],\"modified\":[]},\"repository\":{\"id\":15141344,\"name\":\"tempt\",\"url\":\"https://github.com/moxie0/test\",\"description\":\"test\",\"watchers\":1,\"stargazers\":1,\"forks\":1,\"fork\":false,\"size\":216,\"owner\":{\"name\":\"moxie0\",\"email\":\"moxie@thoughtcrime.org\"},\"private\":false,\"open_issues\":0,\"has_issues\":true,\"has_downloads\":true,\"has_wiki\":true,\"created_at\":1386866024,\"pushed_at\":1387050173,\"master_branch\":\"master\"},\"pusher\":{\"name\":\"moxie0\",\"email\":\"moxie@thoughtcrime.org\"}}";
MultivaluedMapImpl post = new MultivaluedMapImpl();
post.add("payload", payloadValue);
ClientResponse response = client().resource("/v1/github/commits/")
.header("X-Forwarded-For", "192.30.252.1")
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
.post(ClientResponse.class, post);
verify(coinbaseClient).sendPayment(any(Author.class),
eq(BALANCE.multiply(new BigDecimal(0.02))),
anyString());
}
}

1
system.properties Normal file
View File

@ -0,0 +1 @@
java.runtime.version=1.7