From 9b972625e0e343dd1110cc72f03ddba01971a009 Mon Sep 17 00:00:00 2001 From: Teemu Patja Date: Tue, 21 Feb 2017 10:49:25 +0200 Subject: [PATCH] Rewrite Github comment PNG functionality * removed old swing JEditorPane based html to png rendering due to it's lack of support for modern css * new implementation uses wkhtmltoimage (on prod server via wrapper script using xvfb) * no longer generating image on every GET request for QR, but rather generating image when a bounty is cretaed or contract balance changes * images stored to DB * comment image design updated according to UI spec --- env/prod/resources/config.edn | 1 + project.clj | 3 +- ...0170220210009-comment-png-storage.down.sql | 0 .../20170220210009-comment-png-storage.up.sql | 12 +++ resources/sql/queries.sql | 25 +++++- resources/templates/bounty.html | 81 ++++++++++++++++--- src/clj/commiteth/bounties.clj | 10 +++ src/clj/commiteth/db/bounties.clj | 5 +- src/clj/commiteth/db/comment_images.clj | 16 ++++ src/clj/commiteth/db/core.clj | 4 +- src/clj/commiteth/github/core.clj | 4 +- src/clj/commiteth/layout.clj | 5 +- src/clj/commiteth/middleware.clj | 46 +++++------ src/clj/commiteth/routes/qrcodes.clj | 66 +++++---------- src/clj/commiteth/scheduler.clj | 53 +++++++----- src/clj/commiteth/util/images.clj | 36 --------- src/clj/commiteth/util/png_rendering.clj | 48 +++++++++++ 17 files changed, 272 insertions(+), 143 deletions(-) create mode 100644 resources/migrations/20170220210009-comment-png-storage.down.sql create mode 100644 resources/migrations/20170220210009-comment-png-storage.up.sql create mode 100644 src/clj/commiteth/db/comment_images.clj delete mode 100644 src/clj/commiteth/util/images.clj create mode 100644 src/clj/commiteth/util/png_rendering.clj 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" %} + +
+ +
{{issue-url}}
+
ETH
{{balance}}
+
+ +
+
+ + 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)))))