diff --git a/env/prod/resources/config.edn b/env/prod/resources/config.edn
index 2a92f75..097384e 100644
--- a/env/prod/resources/config.edn
+++ b/env/prod/resources/config.edn
@@ -3,4 +3,5 @@
:port 3000
:nrepl-port 7000
:server-address "https://commiteth.com"
+ :html2png-command "/home/commiteth/html2png.sh"
}
diff --git a/project.clj b/project.clj
index 7ddb0f2..28d30ed 100644
--- a/project.clj
+++ b/project.clj
@@ -39,7 +39,8 @@
[bk/ring-gzip "0.2.1"]
[crypto-random "1.2.0"]
[crypto-equality "1.0.0"]
- [cheshire "5.7.0"]]
+ [cheshire "5.7.0"]
+ [mpg "1.3.0"]]
:min-lein-version "2.0.0"
:source-paths ["src/clj" "src/cljc"]
diff --git a/resources/migrations/20170220210009-comment-png-storage.down.sql b/resources/migrations/20170220210009-comment-png-storage.down.sql
new file mode 100644
index 0000000..e69de29
diff --git a/resources/migrations/20170220210009-comment-png-storage.up.sql b/resources/migrations/20170220210009-comment-png-storage.up.sql
new file mode 100644
index 0000000..48a4dae
--- /dev/null
+++ b/resources/migrations/20170220210009-comment-png-storage.up.sql
@@ -0,0 +1,12 @@
+
+-- this column was never used
+ALTER TABLE "repositories" DROP COLUMN IF EXISTS "updated";
+
+-- needde for foreign key
+ALTER TABLE "issues" ADD UNIQUE ("issue_id");
+
+-- table for github PNG comment images
+CREATE TABLE issue_comment (
+id SERIAL PRIMARY KEY,
+issue_id INTEGER REFERENCES issues (issue_id),
+png_data bytea);
diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql
index b16dbc6..0c4f87e 100644
--- a/resources/sql/queries.sql
+++ b/resources/sql/queries.sql
@@ -325,6 +325,8 @@ WHERE r.user_id = :owner_id
FROM pull_requests
WHERE issue_number = i.issue_number);
+-- TODO: misleading name. this is used when updating bounty balances. maybe we should exclude at least bounties that have been paid?
+
-- :name wallets-list :? :*
-- :doc lists all contract ids
SELECT
@@ -332,15 +334,19 @@ SELECT
r.login AS login,
r.repo AS repo,
i.comment_id AS comment_id,
- i.issue_number AS issue_number
+ i.issue_number AS issue_number,
+ i.issue_id AS issue_id,
+ i.balance AS balance
FROM issues i
INNER JOIN repositories r ON r.repo_id = i.repo_id
WHERE contract_address IS NOT NULL;
--- :name get-bounty-address :? :1
+-- :name get-bounty :? :1
SELECT
i.contract_address AS contract_address,
+ i.issue_id AS issue_id,
i.issue_number AS issue_number,
+ i.balance AS balance,
r.login AS login,
r.repo AS repo
FROM issues i
@@ -359,3 +365,18 @@ WHERE contract_address = :contract_address;
UPDATE issues
SET balance = :balance
WHERE contract_address = :contract_address;
+
+
+
+-- :name save-issue-comment-image! :
+
+
-
-
- commiteth
- {{balance}} ETH
- {{address}}
- {{issue-url}}
-
+
+ {% style "/css/style.css" %}
+
+
+
+
diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj
index 160ee65..2974726 100644
--- a/src/clj/commiteth/bounties.clj
+++ b/src/clj/commiteth/bounties.clj
@@ -2,9 +2,11 @@
(:require [commiteth.db.issues :as issues]
[commiteth.db.users :as users]
[commiteth.db.repositories :as repos]
+ [commiteth.db.comment-images :as comment-images]
[commiteth.eth.core :as eth]
[commiteth.github.core :as github]
[commiteth.eth.core :as eth]
+ [commiteth.util.png-rendering :as png-rendering]
[clojure.tools.logging :as log]))
@@ -39,3 +41,11 @@
(count bounty-issues) " existing issues")
(doall
(map (partial add-bounty-for-issue repo repo-id login) bounty-issues))))
+
+(defn update-bounty-comment-image [issue-id issue-url contract-address balance]
+ (let [png-data (png-rendering/gen-comment-image
+ contract-address
+ balance
+ issue-url)]
+ (when png-data
+ (comment-images/save-image! issue-id png-data))))
diff --git a/src/clj/commiteth/db/bounties.clj b/src/clj/commiteth/db/bounties.clj
index 3d57a92..b1c6cc0 100644
--- a/src/clj/commiteth/db/bounties.clj
+++ b/src/clj/commiteth/db/bounties.clj
@@ -54,10 +54,11 @@
(jdbc/with-db-connection [con-db *db*]
(db/update-payout-receipt con-db {:issue_id issue-id :payout_receipt payout-receipt})))
-(defn get-bounty-address
+(defn get-bounty
[user repo issue-number]
(jdbc/with-db-connection [con-db *db*]
- (db/get-bounty-address con-db {:login user :repo repo :issue_number issue-number})))
+ (db/get-bounty con-db {:login user :repo repo :issue_number issue-number})))
+
(defn list-wallets
[]
diff --git a/src/clj/commiteth/db/comment_images.clj b/src/clj/commiteth/db/comment_images.clj
new file mode 100644
index 0000000..6b47e4e
--- /dev/null
+++ b/src/clj/commiteth/db/comment_images.clj
@@ -0,0 +1,16 @@
+(ns commiteth.db.comment-images
+ (:require [commiteth.db.core :refer [*db*] :as db]
+ [clojure.java.jdbc :as jdbc]
+ [clojure.tools.logging :as log]))
+
+(defn save-image!
+ [issue-id png-data]
+ (jdbc/with-db-connection [con-db *db*]
+ (db/save-issue-comment-image! con-db
+ {:issue_id issue-id
+ :png_data png-data})))
+
+(defn get-image-data
+ [issue-id]
+ (jdbc/with-db-connection [con-db *db*]
+ (db/get-issue-comment-image con-db {:issue_id issue-id})))
diff --git a/src/clj/commiteth/db/core.clj b/src/clj/commiteth/db/core.clj
index 2af3561..ab2e49c 100644
--- a/src/clj/commiteth/db/core.clj
+++ b/src/clj/commiteth/db/core.clj
@@ -5,7 +5,8 @@
[conman.core :as conman]
[commiteth.config :refer [env]]
[mount.core :refer [defstate]]
- [migratus.core :as migratus])
+ [migratus.core :as migratus]
+ [mpg.core :as mpg])
(:import org.postgresql.util.PGobject
java.sql.Array
clojure.lang.IPersistentMap
@@ -16,6 +17,7 @@
Timestamp
PreparedStatement]))
+(mpg/patch)
(defn start []
(let [db (env :jdbc-database-url)
diff --git a/src/clj/commiteth/github/core.clj b/src/clj/commiteth/github/core.clj
index dddcf19..08330d0 100644
--- a/src/clj/commiteth/github/core.clj
+++ b/src/clj/commiteth/github/core.clj
@@ -137,7 +137,9 @@
(let [image-url (md-image "QR Code" (get-qr-url user repo issue-number))
balance (str balance " ETH")
site-url (md-url (server-address) (server-address))]
- (format "Current balance: %s\nContract address: %s\n%s\n%s"
+ (format (str "Current balance: %s\n"
+ "Contract address: %s\n"
+ "%s\n%s")
balance contract-address image-url site-url)))
(defn post-comment
diff --git a/src/clj/commiteth/layout.clj b/src/clj/commiteth/layout.clj
index 7ba8c0f..e60f384 100644
--- a/src/clj/commiteth/layout.clj
+++ b/src/clj/commiteth/layout.clj
@@ -7,8 +7,6 @@
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
[commiteth.github.core :as github]))
-(declare ^:dynamic *identity*)
-(declare ^:dynamic *app-context*)
(parser/set-resource-path! (clojure.java.io/resource "templates"))
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
@@ -23,8 +21,7 @@
(assoc params
:authorize-url (github/authorize-url)
:page template
- :csrf-token *anti-forgery-token*
- :servlet-context *app-context*)))
+ :csrf-token *anti-forgery-token*)))
"text/html; charset=utf-8"))
(defn error-page
diff --git a/src/clj/commiteth/middleware.clj b/src/clj/commiteth/middleware.clj
index 680ec67..d60f70c 100644
--- a/src/clj/commiteth/middleware.clj
+++ b/src/clj/commiteth/middleware.clj
@@ -43,16 +43,16 @@
(defn wrap-csrf [handler]
(wrap-anti-forgery
- handler
- {:error-response
- (error-page
- {:status 403
- :title "Invalid anti-forgery token"})}))
+ handler
+ {:error-response
+ (error-page
+ {:status 403
+ :title "Invalid anti-forgery token"})}))
(defn wrap-formats [handler]
(let [wrapped (wrap-restful-format
- handler
- {:formats [:json-kw :transit-json :transit-msgpack]})]
+ handler
+ {:formats [:json-kw :transit-json :transit-msgpack]})]
(fn [request]
;; disable wrap-formats for websockets
;; since they're not compatible with this middleware
@@ -60,8 +60,8 @@
(defn on-error [request response]
(error-page
- {:status 403
- :title (str "Access to " (:uri request) " is not authorized")}))
+ {:status 403
+ :title (str "Access to " (:uri request) " is not authorized")}))
(defn wrap-restricted [handler]
(restrict handler {:handler authenticated?
@@ -75,20 +75,20 @@
(defn wrap-auth [handler]
(let [backend (session-backend)]
(-> handler
- wrap-identity
- (wrap-authentication backend)
- (wrap-authorization backend))))
+ wrap-identity
+ (wrap-authentication backend)
+ (wrap-authorization backend))))
(defn wrap-base [handler]
(-> ((:middleware defaults) handler)
- wrap-auth
-;; wrap-flash
- (wrap-session {:timeout (* 60 60 6)
- :cookie-attrs {:http-only true}})
- (wrap-defaults
- (-> site-defaults
- (assoc-in [:security :anti-forgery] false)
- (dissoc :session)))
- ;; wrap-context
- wrap-gzip
- wrap-internal-error))
+ wrap-auth
+ wrap-flash
+ (wrap-session {:timeout (* 60 60 6)
+ :cookie-attrs {:http-only true}})
+ (wrap-defaults
+ (-> site-defaults
+ (assoc-in [:security :anti-forgery] false)
+ (dissoc :session)))
+ wrap-context
+ wrap-gzip
+ wrap-internal-error))
diff --git a/src/clj/commiteth/routes/qrcodes.clj b/src/clj/commiteth/routes/qrcodes.clj
index a7c662d..863990e 100644
--- a/src/clj/commiteth/routes/qrcodes.clj
+++ b/src/clj/commiteth/routes/qrcodes.clj
@@ -2,56 +2,34 @@
(:require [ring.util.http-response :refer :all]
[compojure.api.sweet :refer :all]
[commiteth.db.bounties :as bounties]
- [commiteth.layout :as layout]
- [commiteth.util.images :refer :all]
- [clj.qrgen :as qr]
- [commiteth.eth.core :as eth]
+ [commiteth.db.comment-images :as comment-images]
[commiteth.github.core :as github]
- [clojure.tools.logging :as log])
- (:import [javax.imageio ImageIO]
- [java.io InputStream]))
-
-(defn ^InputStream generate-qr-code
- [address]
- (qr/as-input-stream
- (qr/from (str "ethereum:" address) :size [256 256])))
-
-(defn generate-html
- [address balance issue-url]
- (:body (layout/render "bounty.html" {:balance balance
- :address address
- :issue-url issue-url})))
-
-(defn generate-image
- [address balance issue-url width height]
- (let [qr-code-image (ImageIO/read (generate-qr-code address))
- comment-image (html->image
- (generate-html address balance issue-url) width height)]
- (combine-images qr-code-image comment-image)))
+ [clojure.tools.logging :as log]
+ [clojure.java.io :as io])
+ (:import [java.io ByteArrayInputStream]))
(defapi qr-routes
(context "/qr" []
(GET "/:owner/:repo/bounty/:issue{[0-9]{1,9}}/:hash/qr.png" [owner repo issue hash]
- (log/debug "qr PNG GET" owner repo issue hash (bounties/get-bounty-address owner
- repo
- (Integer/parseInt issue)))
+ (log/debug "qr PNG GET" owner repo issue hash)
(when-let [{address :contract_address
login :login
repo :repo
- issue-number :issue_number}
- (bounties/get-bounty-address owner
- repo
- (Integer/parseInt issue))]
- (when address
- (let [balance (eth/get-balance-eth address 8)]
- (log/debug "address:" address "balance:" balance)
- (if (and address
- (= hash (github/github-comment-hash owner repo issue)))
- (let [issue-url (str login "/" repo "/issues/" issue-number)
- image-url (generate-image address balance issue-url 768 256)
- response (assoc-in (ok image-url)
- [:headers "cache-control"] "no-cache")]
- (log/debug "balance:" address "response" response)
- response)
- (bad-request))))))))
+ issue-id :issue_id
+ balance :balance}
+ (bounties/get-bounty owner
+ repo
+ (Integer/parseInt issue))]
+ (log/debug "address:" address)
+ (if (and address
+ (= hash (github/github-comment-hash owner repo issue)))
+ (let [{png-data :png_data} (comment-images/get-image-data issue-id)
+ image-byte-stream (ByteArrayInputStream. png-data)
+ response {:status 200
+ :content-type "image/png"
+ :headers {"cache-control" "no-cache"}
+ :body image-byte-stream}]
+ (log/debug "response" response)
+ response)
+ (bad-request))))))
diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj
index 7e54102..d0d1810 100644
--- a/src/clj/commiteth/scheduler.clj
+++ b/src/clj/commiteth/scheduler.clj
@@ -3,7 +3,8 @@
[commiteth.eth.multisig-wallet :as wallet]
[commiteth.github.core :as github]
[commiteth.db.issues :as issues]
- [commiteth.db.bounties :as bounties]
+ [commiteth.db.bounties :as db-bounties]
+ [commiteth.bounties :as bounties]
[clojure.tools.logging :as log]
[mount.core :as mount])
(:import [sun.misc ThreadGroupUtils]
@@ -20,63 +21,75 @@
(log/info "transaction receipt for issue #" issue-id ": " receipt)
(when-let [contract-address (:contractAddress receipt)]
(let [issue (issues/update-contract-address issue-id contract-address)
- {user :login
+ {owner :login
repo :repo
issue-number :issue_number} issue
balance (eth/get-balance-eth contract-address 4)
- {comment-id :id} (github/post-comment user
- repo
- issue-number
- contract-address
- balance)]
- (issues/update-comment-id issue-id comment-id))))))
+ issue-url (str owner "/" repo "/issues/" (str issue-number))]
+ (bounties/update-bounty-comment-image issue-id
+ issue-url
+ contract-address
+ balance)
+ (->> (github/post-comment owner
+ repo
+ issue-number
+ contract-address
+ balance)
+ :id
+ (issues/update-comment-id issue-id)))))))
(defn self-sign-bounty
"Walks through all issues eligible for bounty payout and signs corresponding transaction"
[]
(doseq [{contract-address :contract_address
issue-id :issue_id
- payout-address :payout_address} (bounties/pending-bounties-list)
+ payout-address :payout_address} (db-bounties/pending-bounties-list)
:let [value (eth/get-balance-hex contract-address)]]
(->>
(wallet/execute contract-address payout-address value)
- (bounties/update-execute-hash issue-id))))
+ (db-bounties/update-execute-hash issue-id))))
(defn update-confirm-hash
"Gets transaction receipt for each pending payout and updates confirm_hash"
[]
(doseq [{issue-id :issue_id
- execute-hash :execute_hash} (bounties/pending-payouts-list)]
+ execute-hash :execute_hash} (db-bounties/pending-payouts-list)]
(log/debug "pending payout:" execute-hash)
(when-let [receipt (eth/get-transaction-receipt execute-hash)]
(log/info "execution receipt for issue #" issue-id ": " receipt)
(when-let [confirm-hash (wallet/find-confirmation-hash receipt)]
- (bounties/update-confirm-hash issue-id confirm-hash)))))
+ (db-bounties/update-confirm-hash issue-id confirm-hash)))))
(defn update-payout-hash
"Gets transaction receipt for each confirmed payout and updates payout_hash"
[]
(doseq [{issue-id :issue_id
- payout-hash :payout_hash} (bounties/confirmed-payouts-list)]
+ payout-hash :payout_hash} (db-bounties/confirmed-payouts-list)]
(log/debug "confirmed payout:" payout-hash)
(when-let [receipt (eth/get-transaction-receipt payout-hash)]
(log/info "payout receipt for issue #" issue-id ": " receipt)
- (bounties/update-payout-receipt issue-id receipt))))
+ (db-bounties/update-payout-receipt issue-id receipt))))
(defn update-balance
[]
(doseq [{contract-address :contract_address
- login :login
+ owner :login
repo :repo
comment-id :comment_id
- issue-number :issue_number} (bounties/list-wallets)]
+ issue-id :issue_id
+ old-balance :balance
+ issue-number :issue_number} (db-bounties/list-wallets)]
(when comment-id
- (let [{old-balance :balance} (issues/get-balance contract-address)
- current-balance-hex (eth/get-balance-hex contract-address)
- current-balance-eth (eth/hex->eth current-balance-hex 8)]
+ (let [current-balance-hex (eth/get-balance-hex contract-address)
+ current-balance-eth (eth/hex->eth current-balance-hex 8)
+ issue-url (str owner "/" repo "/issues/" (str issue-number))]
(when-not (= old-balance current-balance-hex)
(issues/update-balance contract-address current-balance-hex)
- (github/update-comment login
+ (bounties/update-bounty-comment-image issue-id
+ issue-url
+ contract-address
+ current-balance-eth)
+ (github/update-comment owner
repo
comment-id
issue-number
diff --git a/src/clj/commiteth/util/images.clj b/src/clj/commiteth/util/images.clj
deleted file mode 100644
index e95ff9b..0000000
--- a/src/clj/commiteth/util/images.clj
+++ /dev/null
@@ -1,36 +0,0 @@
-(ns commiteth.util.images
- (:import [java.awt GraphicsEnvironment RenderingHints]
- [java.awt.image BufferedImage]
- [javax.swing JEditorPane]
- [java.io ByteArrayInputStream ByteArrayOutputStream]
- [javax.imageio ImageIO]))
-
-(defn ^BufferedImage create-image
- [width height]
- (new BufferedImage width height BufferedImage/TYPE_INT_ARGB))
-
-(defn html->image
- [html width height]
- (let [image (create-image width height)
- graphics (.createGraphics image)
- jep (new JEditorPane "text/html" html)]
- (.setRenderingHint graphics
- (RenderingHints/KEY_TEXT_ANTIALIASING)
- (RenderingHints/VALUE_TEXT_ANTIALIAS_GASP))
- (. jep (setSize width height))
- (. jep (print graphics))
- image))
-
-(defn combine-images
- [^BufferedImage image1
- ^BufferedImage image2]
- (let [left-width (.getWidth image1)
- width (+ left-width (.getWidth image2))
- height (max (.getHeight image1) (.getHeight image2))
- combined (create-image width height)
- graphics (.createGraphics combined)]
- (.drawImage graphics image1 nil 0 0)
- (.drawImage graphics image2 nil left-width 0)
- (let [output-stream (ByteArrayOutputStream. 2048)]
- (ImageIO/write combined "png" output-stream)
- (ByteArrayInputStream. (.toByteArray output-stream)))))
diff --git a/src/clj/commiteth/util/png_rendering.clj b/src/clj/commiteth/util/png_rendering.clj
new file mode 100644
index 0000000..e26fef3
--- /dev/null
+++ b/src/clj/commiteth/util/png_rendering.clj
@@ -0,0 +1,48 @@
+(ns commiteth.util.png-rendering
+ (:require [commiteth.layout :refer [render]]
+ [commiteth.config :refer [env]]
+ [commiteth.db.comment-images :as db]
+ [clj.qrgen :as qr]
+ [clojure.data.codec.base64 :as b64]
+ [clojure.tools.logging :as log]
+ [clojure.java.io :as io])
+ (:use [clojure.java.shell :only [sh]])
+ (:import [java.io InputStream]))
+
+
+(defn image->base64 [input-stream]
+ (let [baos (java.io.ByteArrayOutputStream.)]
+ (b64/encoding-transfer input-stream baos)
+ (.toByteArray baos)))
+
+
+(defn ^InputStream generate-qr-image
+ [address]
+ (qr/as-input-stream
+ (qr/from (str "ethereum:" address)
+ :size [255 255])))
+
+
+(defn gen-comment-image [address balance issue-url]
+ (let [qr-image (-> (image->base64 (generate-qr-image address))
+ (String. "ISO-8859-1"))
+ html (:body (render "bounty.html"
+ {:qr-image qr-image
+ :balance balance
+ :address address
+ :issue-url issue-url}))
+ command (env :html2png-command "wkhtmltoimage")
+ {out :out err :err exit :exit}
+ (sh command "-f" "png" "--quality" "80" "--width" "1336" "-" "-"
+ :out-enc :bytes :in html)]
+ (if (= 0 exit)
+ out
+ (do (log/error "Failed to generate PNG file" err)
+ nil))))
+
+
+(defn export-comment-image
+ "Retrieve image PNG from DB and write to file"
+ [issue-id filename]
+ (with-open [w (io/output-stream filename)]
+ (.write w (:png_data (db/get-image-data issue-id)))))