diff --git a/README.md b/README.md index 30330fd..f34b4f3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,22 @@ Opting Out If you'd like to opt out of receiving a payment, simply include the string "FREEBIE" somewhere in your commit message, and you will not receive BTC for that commit. +Payment processing +------------------ + +By default, you need a coinbase account for recieving payment. However, if you add a `Send-bitcoin-reward: `-line to your commit, you recieve payment to that address instead. +You may consider adding a git hook that appends this line for you: + + $ git config --global user.reward-btc "your-btc-address" + $ cat > .git/hooks/commit-msg << "EOF" + #!/bin/sh + VAR=$(git config -z user.reward-btc) + if [ -n "$VAR" ]; then + awk "/Signed-off-by:.*/ && c == 0 {c = 1; print \"Send-bitcoin-reward: $VAR\"}; {print} END{if(c == 0) {print \"Send-bitcoin-reward: $VAR\"}}" $1 > "$1".tmp + mv "$1".tmp $1 + fi + EOF + $ chmod +x .git/hooks/commit-msg Building ------------- diff --git a/src/main/java/org/whispersystems/bithub/client/CoinbaseClient.java b/src/main/java/org/whispersystems/bithub/client/CoinbaseClient.java index aeedbbc..b7ed0f0 100644 --- a/src/main/java/org/whispersystems/bithub/client/CoinbaseClient.java +++ b/src/main/java/org/whispersystems/bithub/client/CoinbaseClient.java @@ -85,7 +85,7 @@ public BigDecimal getExchangeRate() throws IOException { } } - public void sendPayment(Author author, BigDecimal amount, String url) + public void sendPayment(Author author, String overwritingBtcAddress, BigDecimal amount, String url) throws TransferFailedException { try { @@ -95,7 +95,12 @@ public void sendPayment(Author author, BigDecimal amount, String url) String note = "Commit payment:\n__" + author.getUsername() + "__ " + url; - BitcoinTransaction transaction = new BitcoinTransaction(author.getEmail(), + String recipient = author.getEmail(); + if (overwritingBtcAddress != null && overwritingBtcAddress != "") { + recipient = overwritingBtcAddress; + } + + BitcoinTransaction transaction = new BitcoinTransaction(recipient, amount.toPlainString(), note); diff --git a/src/main/java/org/whispersystems/bithub/controllers/GithubController.java b/src/main/java/org/whispersystems/bithub/controllers/GithubController.java index 085a593..dca536e 100644 --- a/src/main/java/org/whispersystems/bithub/controllers/GithubController.java +++ b/src/main/java/org/whispersystems/bithub/controllers/GithubController.java @@ -51,6 +51,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Handles incoming API calls from GitHub. These are currently only @@ -125,12 +127,19 @@ public void handleCommits(@Auth Authentication auth, private void sendPaymentsFor(Repository repository, List commits, BigDecimal balance, BigDecimal exchangeRate) { + Pattern addressPattern = Pattern.compile("^Send-Bitcoin-reward: (.*)$", Pattern.MULTILINE); + for (Commit commit : commits) { try { BigDecimal payout = balance.multiply(payoutRate); if (isViablePaymentAmount(payout)) { - coinbaseClient.sendPayment(commit.getAuthor(), payout, commit.getUrl()); + String authorBtcAddress = getHeaderBtcAddress(commit.getMessage(), addressPattern); + if (authorBtcAddress != null) { + logger.info("Retrieved btc address from commit message: " + authorBtcAddress + "."); + } + coinbaseClient.sendPayment(commit.getAuthor(), authorBtcAddress, payout, + commit.getUrl()); } balance = balance.subtract(payout); @@ -181,6 +190,15 @@ private boolean isViableMessage(String message, String defaultMode) { (message.contains("MONEYMONEY") && defaultMode.equals("FREEBIE")); } + private String getHeaderBtcAddress(String message, Pattern pattern) { + Matcher addressMatcher = pattern.matcher(message); + String btcAddress = null; + while (addressMatcher.find()) { + btcAddress = addressMatcher.group(1); + } + return btcAddress; + } + private boolean isViablePaymentAmount(BigDecimal payment) { return payment.compareTo(new BigDecimal(0)) == 1; } diff --git a/src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java b/src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java index 8de96cc..30f7c75 100644 --- a/src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java +++ b/src/test/java/org/whispersystems/bithub/tests/controllers/GithubControllerTest.java @@ -162,6 +162,24 @@ public void testOptOutCommit() throws Exception, TransferFailedException { .post(ClientResponse.class, post); verify(coinbaseClient, never()).sendPayment(any(Author.class), + anyString(), + any(BigDecimal.class), + anyString()); + } + + @Test + public void testCommitSendToLine() throws Exception, TransferFailedException { + String payloadValue = payload("/payloads/commit_send_to_line.json"); + MultivaluedMapImpl post = new MultivaluedMapImpl(); + post.add("payload", payloadValue); + ClientResponse response = client().resource("/v1/github/commits/") + .header("X-Forwarded-For", "192.30.252.1") + .header("Authorization", authString) + .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) + .post(ClientResponse.class, post); + + verify(coinbaseClient).sendPayment(any(Author.class), + eq("1PRmBDjTcgjR13FPMQ3m4fLhTxo3s4tCkg"), any(BigDecimal.class), anyString()); } @@ -178,6 +196,7 @@ public void testValidCommit() throws Exception, TransferFailedException { .post(ClientResponse.class, post); verify(coinbaseClient).sendPayment(any(Author.class), + anyString(), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); } @@ -194,6 +213,7 @@ public void testNonMaster() throws Exception, TransferFailedException { .post(ClientResponse.class, post); verify(coinbaseClient, never()).sendPayment(any(Author.class), + anyString(), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); } @@ -209,9 +229,9 @@ public void testValidMultipleCommitsMultipleAuthors() throws Exception, Transfer .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE) .post(ClientResponse.class, post); - verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.multiply(new BigDecimal(0.02))), + verify(coinbaseClient, times(1)).sendPayment(any(Author.class), anyString(), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); - verify(coinbaseClient, times(1)).sendPayment(any(Author.class), eq(BALANCE.subtract(BALANCE.multiply(new BigDecimal(0.02))) + verify(coinbaseClient, times(1)).sendPayment(any(Author.class), anyString(), eq(BALANCE.subtract(BALANCE.multiply(new BigDecimal(0.02))) .multiply(new BigDecimal(0.02))), anyString()); } @@ -227,6 +247,7 @@ public void testOptInCommit() throws Exception, TransferFailedException { .post(ClientResponse.class, post); verify(coinbaseClient).sendPayment(any(Author.class), + anyString(), eq(BALANCE.multiply(new BigDecimal(0.02))), anyString()); } @@ -243,6 +264,7 @@ public void testNoOptInCommit() throws Exception, TransferFailedException { .post(ClientResponse.class, post); verify(coinbaseClient, never()).sendPayment(any(Author.class), + anyString(), any(BigDecimal.class), anyString()); } diff --git a/src/test/resources/payloads/commit_send_to_line.json b/src/test/resources/payloads/commit_send_to_line.json new file mode 100644 index 0000000..5db33f2 --- /dev/null +++ b/src/test/resources/payloads/commit_send_to_line.json @@ -0,0 +1,79 @@ +{ + "after": "bcf09f8b4a32921114587e4814a3f0849aa9900f", + "before": "1b141aa068165dd1ed376f483cd5fdc2c64f32b1", + "commits": [ + { + "added": [], + "author": { + "email": "moxie@thoughtcrime.org", + "name": "Moxie Marlinspike", + "username": "moxie0" + }, + "committer": { + "email": "moxie@thoughtcrime.org", + "name": "Moxie Marlinspike", + "username": "moxie0" + }, + "distinct": true, + "id": "ba1b681c71db4fcd461954b1bf344bc6e29411e5", + "message": "Update path\n\nsome-description\nSend-Bitcoin-reward: 1PRmBDjTcgjR13FPMQ3m4fLhTxo3s4tCkg", + "modified": [ + "README.md" + ], + "removed": [], + "timestamp": "2013-12-14T11:42:28-08:00", + "url": "https://github.com/moxie0/tempt/commit/ba1b681c71db4fcd461954b1bf344bc6e29411e5" + }], + "compare": "https://github.com/moxie0/tempt/compare/1b141aa06816...bcf09f8b4a32", + "created": false, + "deleted": false, + "forced": false, + "head_commit": { + "added": [], + "author": { + "email": "moxie@thoughtcrime.org", + "name": "Moxie Marlinspike", + "username": "moxie0" + }, + "committer": { + "email": "moxie@thoughtcrime.org", + "name": "Moxie Marlinspike", + "username": "moxie0" + }, + "distinct": true, + "id": "bcf09f8b4a32921114587e4814a3f0849aa9900f", + "message": "Merge branch 'master' of github.com:moxie0/tempt", + "modified": [], + "removed": [], + "timestamp": "2013-12-14T11:42:44-08:00", + "url": "https://github.com/moxie0/tempt/commit/bcf09f8b4a32921114587e4814a3f0849aa9900f" + }, + "pusher": { + "email": "moxie@thoughtcrime.org", + "name": "moxie0" + }, + "ref": "refs/heads/master", + "repository": { + "created_at": 1386866024, + "description": "test", + "fork": false, + "forks": 1, + "has_downloads": true, + "has_issues": true, + "has_wiki": true, + "id": 15141344, + "master_branch": "master", + "name": "tempt", + "open_issues": 0, + "owner": { + "email": "moxie@thoughtcrime.org", + "name": "moxie0" + }, + "private": false, + "pushed_at": 1387050173, + "size": 216, + "stargazers": 1, + "url": "https://github.com/moxie0/test", + "watchers": 1 + } +}