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
This commit is contained in:
Teemu Patja 2017-02-21 10:49:25 +02:00
parent bb5d492631
commit 9b972625e0
No known key found for this signature in database
GPG Key ID: F5B7035E6580FD4C
17 changed files with 272 additions and 143 deletions

View File

@ -3,4 +3,5 @@
:port 3000
:nrepl-port 7000
:server-address "https://commiteth.com"
:html2png-command "/home/commiteth/html2png.sh"
}

View File

@ -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"]

View File

@ -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);

View File

@ -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! :<! :1
INSERT INTO issue_comment (issue_id, png_data)
VALUES (:issue_id, :png_data)
ON CONFLICT (issue_id) DO UPDATE
SET png_data = :png_data
RETURNING id;
-- :name get-issue-comment-image :? :1
SELECT png_data
FROM issue_comment
WHERE issue_id = :issue_id;

View File

@ -1,10 +1,73 @@
<!-- This html file is converted into png image so please keep it simple.
HTMLEditorKit doesn't support 'float' and 'display' css attributes.-->
<html>
<body>
<div style="font-family: sans-serif; font-size: 21px;">
<br/>
commiteth<br/>
<span style="font-size: 27px;">{{balance}} ETH</span><br/><br>
{{address}}<br/>
<span style="color: #5FC48D;">{{issue-url}}</span>
</div>
<style>
@import url('https://fonts.googleapis.com/css?family=Roboto:300&subset=latin');
.github-comment {
font-family: 'Roboto', sans-serif;
border: #e7e7e7 solid 0.1em!important;
border-radius: 8px!important;
margin: 0px;
padding: 1em;
width: 1336px;
height: 300px;
}
.commiteth-logo {
position: fixed;
top: 32px;
left: 32px;
}
.comment-issue-url {
font-size: 1.1em;
color: #a8aab1;
position: fixed;
top: 155px;
left: 32px;
}
.comment-eth {
color: #a8aab1;
font-size: 3em;
position: fixed;
top: 196px;
left: 32px;
}
.comment-balance {
color: #343434;
position: fixed;
top: 196px;
left: 135px;
}
.qr-image {
position: fixed;
padding: 0px;
margin: 0px;
top: 24px;
left: 1072px;
border: none;
background-color: red;
}
.qr-image>img {
padding: 0px;
margin: 0px;
background-color: yellow;
}
</style>
{% style "/css/style.css" %}
<div class="github-comment">
<div class="commiteth-logo">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAABT1JREFUaAXdWmtMHFUUPmcWZEVRYrDSyqNYozb+sSkxgsvysLawJfpDa1ob44/+Mjb+MDH6h/hKjMbExBKNMekPTdQmjYkvFpXIcxtNRekfY0RSnr4oMUDpspSdOZ47eLcg9+7OsrvDyE3g7px7znfPN3PuuY8ZgC1SMFs89g4OFl6YWawlojK0YLsFVMrgpYB0Vbb6EDhEjA4wDL78k1PNtSMSOyMiu77qK79sYisBtSJQExH4JXDua4z6DNo33tLwregrbyMdlofP7EKIv7gUhyMAliEwaCNAGdlQoUnwBkPUCJi0iJR/HdkBcbMNIH7MIsrPyI8sGHM47ZEwjolUdvaFrLj5EY+B66TxZtccyqPSBzss5IWurgj3PWta8LmXSKz4ii9Ln5MO9p09o35zceIkE3hUGjiuETkHwAIiLLBN1oYQg3LWwmEEbJ8MBT+R/iQNrXh0/F1WdEYCccgA7AIDuyzw/VhzoGbuNKIpO8p1rX0iZeH+40y+PaUDiJ1885+fbGn4PqVuDhWURCo6IwEis5tDKklmwl85jz8u83gOfXQEvY7Izp6hYjM69zMHdakOgY2+9BVef2Sscc+sTsdt+bqsFV+ceyo5CXz7WKj+oJdIiJu25oncHokURefNMQ6pG5R3FLG7piW4381BrPRDIVzzRBYvmk/qScCUH/CwF0kIXgkiYs6wCJ5WkLVFaBjHR0LBC7r2zZYniFhLUwFeI9+ocogntZ8mDtR9pmrziixBBCy6X+cUovEqipnawyVBhDdCSiJMYP4Wf90pD3OwXbOJ3NYzWMJXd6mc5bVNf28jxlVtXpLZRJZil+4We0iVYxxW3Sq512T2opGzlXYW5w3gQKZOV31zpjJ+2bzDAPJlimWiYRl5MDyxr+78aiybCGelbbxJURbDZ/2mbHAgFAcSf00vvLccW35YqGdnKWyCxUBl4d4v/OA/OhK6Z15g26FFFm0TF6pSUlI4o5I7kU1PX3qJdyI2CSf6aekQtMZg6XVpYxPhzKQkwoNm9ofq6mWpnH5ND6Vvk47FFXybCB8kaAIrHdDN1V15IgDKpQezK+Y4T7InSeU8fpxKI7P2K/grRAyc1gHOzETFHLOhQsX5bZxITm/IOKURdvih4BmpZmctDiwtEcs0bmblP6RBOvVUbe0i6z/iWvo1EP7kUzt1Maw6bhhUNzqTjt537zhrir+cFTu0CvzXnAXNopDIaspZ71kEtokMN1aLueKcEpewvqGHkh4bKe1cFtpERJ/2mZSycyo6Hxs4rGzykDBBRBys6fzi8HqOpxrlolJn47Y8QcQoKIvwOFHPJwR3VnQOPOi2c+n0lyAy1lgV4+wl3jcoC4HVfmu4X7kVVhq4LEwQEf1eXeR7i9ddfyt9ICiLAZ06RJkvxZX4GQrXEPklELjIO8I3tZhETd+F+0+8QLTGTqvvYsO6AbxljkzFUSgaeYc4xLTLd14ENMej82crO3vt93cu3nhtV+ueiNTcEq8VEmQ6et/n34/J6xT1OR44Yi7q8hdeO7QD9s66efqifSLC6YxevdmsMcrH5AvIM6p9Kf9p1nWymWvdEpYb/n31ZuCJyebgp9ImKRGpJF6G8knLK4zvsWyFR6cO1n8o/HTk2ESo/jWfDx8Qp46SnDdqapN+OCIilMebgx2Q59vNZN5JltEksBs17z6rZD+OiQiDyf2B3ydD9U/wBxO7DcQP+D3R2tiXqC7VPJCGZFeOxohU/m/tpY9qMiKymljiMycTyvlLIf68ydrOK/+b3PrMabUv/+vf/wB8Edrv6oR/RwAAAABJRU5ErkJggg=="/>
</div>
<div class="comment-issue-url">{{issue-url}}</div>
<div class="comment-eth">ETH<div class="comment-balance">{{balance}}</div></div>
<div class="qr-image">
<img src="data:image/png;base64,{{qr-image}}"/>
</div>
</div>
</body>
</html>

View File

@ -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))))

View File

@ -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
[]

View File

@ -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})))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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))))))

View File

@ -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

View File

@ -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)))))

View File

@ -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)))))