Add dashboard
1) Move cache polling out of StatusController. 2) Simplify view logic. 3) Make recent transaction json output work. 4) Add dashboard view.
This commit is contained in:
parent
e437de4f15
commit
f1cc87dcea
2
Procfile
2
Procfile
@ -1 +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 -Ddw.github.webhook.password=$GITHUB_WEBHOOK_PASSWORD -jar target/BitHub-0.1.jar server
|
||||
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.github.webhook.password=$GITHUB_WEBHOOK_PASSWORD -Ddw.organization.name="$ORGANIZATION_NAME" -Ddw.organization.donationUrl=$DONATION_URL -jar target/BitHub-0.1.jar server
|
||||
|
||||
@ -38,6 +38,8 @@ $ 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
|
||||
```
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
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.
|
||||
|
||||
5
pom.xml
5
pom.xml
@ -56,6 +56,11 @@
|
||||
<artifactId>dropwizard-views</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-views-mustache</artifactId>
|
||||
<version>${dropwizard.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-servlets</artifactId>
|
||||
|
||||
@ -21,6 +21,7 @@ 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;
|
||||
@ -43,6 +44,11 @@ public class BithubServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
private BithubConfiguration bithub = new BithubConfiguration();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private OrganizationConfiguration organization;
|
||||
|
||||
|
||||
public GithubConfiguration getGithubConfiguration() {
|
||||
return github;
|
||||
@ -56,4 +62,7 @@ public class BithubServerConfiguration extends Configuration {
|
||||
return bithub;
|
||||
}
|
||||
|
||||
public OrganizationConfiguration getOrganizationConfiguration() {
|
||||
return organization;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,10 +22,12 @@ import org.whispersystems.bithub.auth.GithubWebhookAuthenticator;
|
||||
import org.whispersystems.bithub.client.CoinbaseClient;
|
||||
import org.whispersystems.bithub.client.GithubClient;
|
||||
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;
|
||||
@ -60,14 +62,22 @@ public class BithubService extends Application<BithubServerConfiguration> {
|
||||
String githubWebhookPwd = config.getGithubConfiguration().getWebhookConfiguration().getPassword();
|
||||
List<RepositoryConfiguration> githubRepositories = config.getGithubConfiguration().getRepositories();
|
||||
BigDecimal payoutRate = config.getBithubConfiguration().getPayoutRate();
|
||||
GithubClient githubClient = new GithubClient(githubUser, githubToken);
|
||||
CoinbaseClient coinbaseClient = new CoinbaseClient(config.getCoinbaseConfiguration().getApiKey());
|
||||
String organizationName = config.getOrganizationConfiguration().getName();
|
||||
String donationUrl = config.getOrganizationConfiguration().getDonationUrl().toExternalForm();
|
||||
|
||||
GithubClient githubClient = new GithubClient(githubUser, githubToken);
|
||||
CoinbaseClient coinbaseClient = new CoinbaseClient(config.getCoinbaseConfiguration().getApiKey());
|
||||
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(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),
|
||||
|
||||
@ -30,8 +30,8 @@ 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 org.whispersystems.bithub.entities.CoinbseRecentTransactionsResponse;
|
||||
import org.whispersystems.bithub.entities.CoinbaseTransaction;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
@ -59,12 +59,12 @@ public class CoinbaseClient {
|
||||
this.client = Client.create(getClientConfig());
|
||||
}
|
||||
|
||||
public List<Transaction> getRecentTransactions() throws IOException {
|
||||
public List<CoinbaseTransaction> getRecentTransactions() throws IOException {
|
||||
try {
|
||||
return client.resource(COINBASE_URL)
|
||||
.path(RECENT_TRANSACTIONS_PATH)
|
||||
.queryParam("api_key", apiKey)
|
||||
.get(RecentTransactionsResponse.class).getTransactions();
|
||||
.get(CoinbseRecentTransactionsResponse.class).getTransactions();
|
||||
} catch (UniformInterfaceException | ClientHandlerException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
@ -42,8 +42,10 @@ import javax.ws.rs.core.MediaType;
|
||||
*/
|
||||
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 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);
|
||||
|
||||
@ -55,6 +57,37 @@ public class GithubClient {
|
||||
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(),
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -20,28 +20,27 @@ package org.whispersystems.bithub.controllers;
|
||||
import com.codahale.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.config.RepositoryConfiguration;
|
||||
import org.whispersystems.bithub.entities.Repositories;
|
||||
import org.whispersystems.bithub.entities.Repository;
|
||||
import org.whispersystems.bithub.entities.Transaction;
|
||||
import org.whispersystems.bithub.util.Badge;
|
||||
import org.whispersystems.bithub.views.RecentTransactionsView;
|
||||
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.Consumes;
|
||||
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.CacheControl;
|
||||
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.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.jersey.caching.CacheControl;
|
||||
|
||||
/**
|
||||
* Handles incoming API calls for BitHub instance status information.
|
||||
@ -51,123 +50,65 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
@Path("/v1/status")
|
||||
public class StatusController {
|
||||
|
||||
private static final int UPDATE_FREQUENCY_MILLIS = 60 * 1000;
|
||||
private final Logger logger = LoggerFactory.getLogger(StatusController.class);
|
||||
|
||||
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
|
||||
private final Logger logger = LoggerFactory.getLogger(StatusController.class);
|
||||
private final List<RepositoryConfiguration> repositoryConfiguration;
|
||||
private final CacheManager coinbaseManager;
|
||||
|
||||
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);
|
||||
public StatusController(CacheManager coinbaseManager,
|
||||
List<RepositoryConfiguration> repositoryConfiguration)
|
||||
throws IOException
|
||||
{
|
||||
this.coinbaseManager = coinbaseManager;
|
||||
this.repositoryConfiguration = repositoryConfiguration;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_HTML})
|
||||
public RecentTransactionsView getTransactions()
|
||||
throws IOException
|
||||
public Response getTransactions(@QueryParam("format") @DefaultValue("html") String format)
|
||||
throws IOException
|
||||
{
|
||||
return cachedTransactions.get();
|
||||
List<Transaction> 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<Repository> 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
|
||||
{
|
||||
CacheControl cacheControl = new CacheControl();
|
||||
cacheControl.setNoCache(true);
|
||||
CurrentPayment currentPayment = coinbaseManager.getCurrentPaymentAmount();
|
||||
|
||||
switch (format) {
|
||||
case "json":
|
||||
return Response.ok(cachedPaymentStatus.get().getEntity(), MediaType.APPLICATION_JSON_TYPE).cacheControl(cacheControl).build();
|
||||
return Response.ok(currentPayment.getEntity(), MediaType.APPLICATION_JSON_TYPE).build();
|
||||
case "png_small":
|
||||
return Response.ok(cachedPaymentStatus.get().getSmallBadge(), "image/png").cacheControl(cacheControl).build();
|
||||
return Response.ok(currentPayment.getSmallBadge(), "image/png").build();
|
||||
default:
|
||||
return Response.ok(cachedPaymentStatus.get().getBadge(), "image/png").cacheControl(cacheControl).build();
|
||||
return Response.ok(currentPayment.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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CoinbaseTransaction {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
public boolean isSentTransaction() {
|
||||
BigDecimal amount = new BigDecimal(getAmount());
|
||||
return amount.compareTo(new BigDecimal(0.0)) < 0;
|
||||
}
|
||||
|
||||
}
|
||||
@ -23,12 +23,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TransactionWrapper {
|
||||
public class CoinbaseTransactionWrapper {
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Transaction transaction;
|
||||
private CoinbaseTransaction transaction;
|
||||
|
||||
public Transaction getTransaction() {
|
||||
public CoinbaseTransaction getTransaction() {
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
@ -25,16 +25,16 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class RecentTransactionsResponse {
|
||||
public class CoinbseRecentTransactionsResponse {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private List<TransactionWrapper> transactions;
|
||||
private List<CoinbaseTransactionWrapper> transactions;
|
||||
|
||||
public List<Transaction> getTransactions() {
|
||||
List<Transaction> rawTransactions = new LinkedList<Transaction>();
|
||||
public List<CoinbaseTransaction> getTransactions() {
|
||||
List<CoinbaseTransaction> rawTransactions = new LinkedList<CoinbaseTransaction>();
|
||||
|
||||
for (TransactionWrapper transactionWrapper : transactions) {
|
||||
for (CoinbaseTransactionWrapper transactionWrapper : transactions) {
|
||||
rawTransactions.add(transactionWrapper.getTransaction());
|
||||
}
|
||||
|
||||
@ -26,4 +26,8 @@ public class Payment {
|
||||
public Payment(String payment) {
|
||||
this.payment = payment;
|
||||
}
|
||||
|
||||
public String getPayment() {
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package org.whispersystems.bithub.entities;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Repositories {
|
||||
|
||||
public List<Repository> repositories;
|
||||
|
||||
public Repositories() {}
|
||||
|
||||
public Repositories(List<Repository> repositories) {
|
||||
this.repositories = repositories;
|
||||
}
|
||||
|
||||
}
|
||||
@ -38,6 +38,15 @@ public class Repository {
|
||||
@NotEmpty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String description;
|
||||
|
||||
public Repository() {}
|
||||
|
||||
public Repository(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Author getOwner() {
|
||||
return owner;
|
||||
}
|
||||
@ -49,4 +58,8 @@ public class Repository {
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,58 +1,62 @@
|
||||
/**
|
||||
* 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
|
||||
private String destination;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private Amount amount;
|
||||
|
||||
@JsonProperty(value = "recipient_address")
|
||||
private String recipientAddress;
|
||||
private String amount;
|
||||
|
||||
@JsonProperty
|
||||
private String notes;
|
||||
private String commitUrl;
|
||||
|
||||
public String getCreatedTime() {
|
||||
return createdTime;
|
||||
@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.getAmount();
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getRecipientAddress() {
|
||||
return recipientAddress;
|
||||
public String getCommitUrl() {
|
||||
return commitUrl;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
public String getCommitSha() {
|
||||
return commitSha;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package org.whispersystems.bithub.entities;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Transactions {
|
||||
|
||||
@JsonProperty
|
||||
private List<Transaction> transactions;
|
||||
|
||||
public Transactions() {}
|
||||
|
||||
public Transactions(List<Transaction> transactions) {
|
||||
this.transactions = transactions;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
package org.whispersystems.bithub.storage;
|
||||
|
||||
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.CoinbaseTransaction;
|
||||
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<RepositoryConfiguration> repositories;
|
||||
|
||||
private AtomicReference<CurrentPayment> cachedPaymentStatus;
|
||||
private AtomicReference<List<Transaction>> cachedTransactions;
|
||||
private AtomicReference<List<Repository>> cachedRepositories;
|
||||
|
||||
public CacheManager(CoinbaseClient coinbaseClient,
|
||||
GithubClient githubClient,
|
||||
List<RepositoryConfiguration> 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<Transaction> getRecentTransactions() {
|
||||
return cachedTransactions.get();
|
||||
}
|
||||
|
||||
public CurrentPayment getCurrentPaymentAmount() {
|
||||
return cachedPaymentStatus.get();
|
||||
}
|
||||
|
||||
public List<Repository> getRepositories() {
|
||||
return cachedRepositories.get();
|
||||
}
|
||||
|
||||
public void initializeUpdates(final CoinbaseClient coinbaseClient,
|
||||
final GithubClient githubClient,
|
||||
final List<RepositoryConfiguration> repoConfigs)
|
||||
{
|
||||
executor.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
CurrentPayment currentPayment = createCurrentPaymentForBalance(coinbaseClient);
|
||||
List<Transaction> transactions = createRecentTransactions (coinbaseClient);
|
||||
List<Repository> repositories = createRepositories(githubClient, repoConfigs);
|
||||
|
||||
cachedPaymentStatus.set(currentPayment);
|
||||
cachedTransactions.set(transactions);
|
||||
cachedRepositories.set(repositories);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to update badge", e);
|
||||
}
|
||||
}
|
||||
}, UPDATE_FREQUENCY_MILLIS, UPDATE_FREQUENCY_MILLIS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private List<Repository> createRepositories(GithubClient githubClient,
|
||||
List<RepositoryConfiguration> configured)
|
||||
{
|
||||
List<Repository> repositoryList = new LinkedList<>();
|
||||
|
||||
for (RepositoryConfiguration repository : configured) {
|
||||
repositoryList.add(githubClient.getRepository(repository.getUrl()));
|
||||
}
|
||||
|
||||
return repositoryList;
|
||||
}
|
||||
|
||||
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 List<Transaction> createRecentTransactions(CoinbaseClient coinbaseClient)
|
||||
throws IOException
|
||||
{
|
||||
List<CoinbaseTransaction> recentTransactions = coinbaseClient.getRecentTransactions();
|
||||
BigDecimal exchangeRate = coinbaseClient.getExchangeRate();
|
||||
List<Transaction> transactions = new LinkedList<>();
|
||||
|
||||
for (CoinbaseTransaction coinbaseTransaction : recentTransactions) {
|
||||
try {
|
||||
if (coinbaseTransaction.isSentTransaction()) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package org.whispersystems.bithub.storage;
|
||||
|
||||
import org.apache.commons.lang3.StringEscapeUtils;
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
import org.whispersystems.bithub.entities.CoinbaseTransaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class CoinbaseTransactionParser {
|
||||
|
||||
private final CoinbaseTransaction coinbaseTransaction;
|
||||
|
||||
public CoinbaseTransactionParser(CoinbaseTransaction coinbaseTransaction) {
|
||||
this.coinbaseTransaction = coinbaseTransaction;
|
||||
}
|
||||
|
||||
public String parseAmountInDollars(BigDecimal exchangeRate) {
|
||||
return new BigDecimal(coinbaseTransaction.getAmount()).abs()
|
||||
.multiply(exchangeRate)
|
||||
.setScale(2, RoundingMode.CEILING)
|
||||
.toPlainString();
|
||||
}
|
||||
|
||||
public String parseTimestamp() throws ParseException {
|
||||
String timestamp = coinbaseTransaction.getCreatedTime();
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
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<Repository> repositories;
|
||||
private final List<Transaction> transactions;
|
||||
|
||||
public DashboardView(String organizationName, String donationUrl,
|
||||
CurrentPayment currentPayment,
|
||||
List<Repository> repositories,
|
||||
List<Transaction> 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<Repository> getRepositories() {
|
||||
return repositories;
|
||||
}
|
||||
|
||||
public List<Transaction> getTransactions() {
|
||||
return transactions;
|
||||
}
|
||||
|
||||
public String getRepositoriesCount() {
|
||||
return String.valueOf(repositories.size());
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ package org.whispersystems.bithub.views;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.bithub.entities.CoinbaseTransaction;
|
||||
import org.whispersystems.bithub.entities.Transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@ -33,34 +34,16 @@ import io.dropwizard.views.View;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class RecentTransactionsView extends View {
|
||||
public class TransactionsView extends View {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RecentTransactionsView.class);
|
||||
private final List<TransactionView> transactions = new LinkedList<>();
|
||||
private final List<Transaction> transactions;
|
||||
|
||||
public RecentTransactionsView(List<Transaction> recentTransactions, BigDecimal exchangeRate) {
|
||||
public TransactionsView(List<Transaction> transactions) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
this.transactions = transactions;
|
||||
}
|
||||
|
||||
private boolean isSentTransaction(Transaction transaction) {
|
||||
BigDecimal amount = new BigDecimal(transaction.getAmount());
|
||||
return amount.compareTo(new BigDecimal(0.0)) < 0;
|
||||
}
|
||||
|
||||
public List<TransactionView> getTransactions() {
|
||||
public List<Transaction> getTransactions() {
|
||||
return transactions;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link rel="icon" href="../../favicon.ico">
|
||||
|
||||
<title>BitHub :: Dashboard </title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
background-color: transparent;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* top tags */
|
||||
|
||||
.hero-widget { text-align: center; padding-top: 20px; padding-bottom: 20px; }
|
||||
.hero-widget .icon { display: block; font-size: 96px; line-height: 96px; margin-bottom: 10px; text-align: center; }
|
||||
.hero-widget var { display: block; height: 64px; font-size: 64px; line-height: 64px; font-style: normal; }
|
||||
.hero-widget label { font-size: 17px; }
|
||||
.hero-widget .options { margin-top: 10px; }
|
||||
|
||||
/* repositories */
|
||||
|
||||
.repository {
|
||||
margin-top: 20px;
|
||||
padding: 40px 0px 20px 0px;
|
||||
background-color: #f7f7f7;
|
||||
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
|
||||
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.repository-name {
|
||||
font-size: 50px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.repository-name a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.repository-description {
|
||||
font-size: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<h1>BitHub</h1>
|
||||
<p class="lead">{{organizationName}}</p>
|
||||
<p><a class="btn btn-lg btn-success" href="{{donationUrl}}" role="button">Donate BTC today</a></p>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="hero-widget well well-sm">
|
||||
<div class="icon">
|
||||
<i class="glyphicon glyphicon-usd"></i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<var>{{payment}}</var>
|
||||
<label class="text-muted">USD per commit</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="hero-widget well well-sm">
|
||||
<div class="icon">
|
||||
<i class="glyphicon glyphicon-tags"></i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<var>0</var>
|
||||
<label class="text-muted">open bounties</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="hero-widget well well-sm">
|
||||
<div class="icon">
|
||||
<i class="glyphicon glyphicon-book"></i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<var>{{repositoriesCount}}</var>
|
||||
<label class="text-muted">repositories</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="sub-header">Recent payments</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>USD</th>
|
||||
<th>Author</th>
|
||||
<th>Description</th>
|
||||
<th>Commit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="transactions_target">
|
||||
{{#transactions}}
|
||||
<tr>
|
||||
<td>${{amount}} USD</td>
|
||||
<td><a href="https://github.com/{{destination}}">{{destination}}</a></td>
|
||||
<td><pre>{{description}}</pre></td>
|
||||
<td><a href="{{commitUrl}}">{{commitSha}}</a></td>
|
||||
</tr>
|
||||
{{/transactions}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="sub-header">Repositories</h2>
|
||||
<div class="row">
|
||||
{{#repositories}}
|
||||
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6">
|
||||
<div class="repository">
|
||||
<div class="repository-name">
|
||||
<a href="{{url}}">{{name}}</a>
|
||||
</div>
|
||||
<div class="repository-description">
|
||||
<p>{{description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/repositories}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -4,15 +4,20 @@ import com.sun.jersey.api.client.ClientResponse;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.whispersystems.bithub.client.CoinbaseClient;
|
||||
import org.whispersystems.bithub.client.GithubClient;
|
||||
import org.whispersystems.bithub.config.RepositoryConfiguration;
|
||||
import org.whispersystems.bithub.controllers.StatusController;
|
||||
import org.whispersystems.bithub.entities.RecentTransactionsResponse;
|
||||
import org.whispersystems.bithub.entities.CoinbseRecentTransactionsResponse;
|
||||
import org.whispersystems.bithub.entities.Repository;
|
||||
import org.whispersystems.bithub.storage.CacheManager;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import io.dropwizard.testing.junit.ResourceTestRule;
|
||||
import static org.fest.assertions.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.bithub.tests.util.JsonHelper.fromJson;
|
||||
@ -20,25 +25,31 @@ import static org.whispersystems.bithub.tests.util.JsonHelper.jsonFixture;
|
||||
|
||||
public class StatusControllerTest {
|
||||
|
||||
private static final BigDecimal PAYOUT_RATE = new BigDecimal(0.02);
|
||||
private static final BigDecimal BALANCE = new BigDecimal(10.01);
|
||||
private static final BigDecimal EXCHANGE_RATE = new BigDecimal(1.0);
|
||||
private static final BigDecimal PAYOUT_RATE = new BigDecimal(0.02 );
|
||||
private static final BigDecimal BALANCE = new BigDecimal(10.01);
|
||||
private static final BigDecimal EXCHANGE_RATE = new BigDecimal(1.0 );
|
||||
|
||||
private static final CoinbaseClient coinbaseClient = mock(CoinbaseClient.class);
|
||||
private static final GithubClient githubClient = mock(GithubClient.class );
|
||||
|
||||
@ClassRule
|
||||
public static ResourceTestRule resources;
|
||||
|
||||
static {
|
||||
try {
|
||||
when(coinbaseClient.getRecentTransactions()).thenReturn(fromJson(jsonFixture("payloads/transactions.json"), RecentTransactionsResponse.class).getTransactions());
|
||||
when(coinbaseClient.getRecentTransactions()).thenReturn(fromJson(jsonFixture("payloads/transactions.json"), CoinbseRecentTransactionsResponse.class).getTransactions());
|
||||
when(coinbaseClient.getAccountBalance()).thenReturn(BALANCE);
|
||||
when(coinbaseClient.getExchangeRate()).thenReturn(EXCHANGE_RATE);
|
||||
|
||||
CacheManager coinbaseManager = new CacheManager(coinbaseClient, githubClient,
|
||||
new LinkedList<RepositoryConfiguration>(),
|
||||
PAYOUT_RATE);
|
||||
coinbaseManager.start();
|
||||
|
||||
resources = ResourceTestRule.builder()
|
||||
.addResource(new StatusController(coinbaseClient, PAYOUT_RATE))
|
||||
.addResource(new StatusController(coinbaseManager, null))
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
@ -62,7 +73,7 @@ public class StatusControllerTest {
|
||||
|
||||
@Test
|
||||
public void testTransactionsJson() throws Exception {
|
||||
ClientResponse response = resources.client().resource("/v1/status/transactions/").accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
ClientResponse response = resources.client().resource("/v1/status/transactions/?format=json").accept(MediaType.APPLICATION_JSON_TYPE)
|
||||
.get(ClientResponse.class);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user