Issue and PR webhooks

This commit is contained in:
kagel 2016-08-28 23:16:45 +03:00
parent a6459a2a32
commit 4a5196987b
16 changed files with 254 additions and 67 deletions

View File

@ -1,3 +1,4 @@
DROP TABLE users; DROP TABLE users;
DROP TABLE repositories; DROP TABLE repositories;
DROP TABLE issues; DROP TABLE issues;
DROP TABLE pull_requests;

View File

@ -1,31 +1,38 @@
CREATE TABLE users ( CREATE TABLE users
id VARCHAR(40) PRIMARY KEY, -- user id (
login VARCHAR(64) UNIQUE NOT NULL, -- github login id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(128), -- user name login VARCHAR(64) NOT NULL,
email VARCHAR(128), -- user email, if present name VARCHAR(128),
token VARCHAR(40) NOT NULL, -- github oauth token email VARCHAR(128),
address VARCHAR(42), -- ETH address token VARCHAR(40),
created TIME -- user created date address VARCHAR(42),
created TIME
); );
CREATE UNIQUE INDEX users_login_key ON users (login);
CREATE TABLE repositories CREATE TABLE repositories
( (
login VARCHAR(64) NOT NULL, -- github user repo_id INTEGER PRIMARY KEY NOT NULL,
repo VARCHAR(64) NOT NULL, -- github repo user_id INTEGER,
updated TIME, -- date of the last crawl login VARCHAR(64) NOT NULL,
repo_id INTEGER NOT NULL, -- github repository id repo VARCHAR(64) NOT NULL,
enabled BOOLEAN DEFAULT TRUE updated TIME,
enabled BOOLEAN DEFAULT TRUE,
hook_id INTEGER
); );
CREATE UNIQUE INDEX repositories_user_repo_pk ON repositories (login, repo);
CREATE UNIQUE INDEX repositories_repo_id_pk ON repositories (repo_id);
CREATE INDEX repositories_login_repo_index ON repositories (login, repo);
CREATE INDEX repositories_repo_id_index ON repositories (repo_id);
CREATE TABLE issues CREATE TABLE issues
( (
repo_id INTEGER NOT NULL, issue_id INTEGER PRIMARY KEY NOT NULL,
issue_id INTEGER NOT NULL, repo_id INTEGER NOT NULL,
address VARCHAR(42), address VARCHAR(256),
CONSTRAINT issues_repo_id_issue_id_pk PRIMARY KEY (repo_id, issue_id) issue_number INTEGER,
commit_id VARCHAR(40)
);
CREATE TABLE pull_requests
(
pr_id INTEGER PRIMARY KEY NOT NULL,
repo_id INTEGER,
user_id INTEGER,
parents VARCHAR(4099) -- 100 commit SHAs + 99 commas
); );

View File

@ -4,7 +4,17 @@
-- :doc creates a new user record -- :doc creates a new user record
INSERT INTO users INSERT INTO users
(id, login, name, email, token, address, created) (id, login, name, email, token, address, created)
VALUES (:id, :login, :name, :email, :token, :address, :created); SELECT
:id,
:login,
:name,
:email,
:token,
:address,
:created
WHERE NOT exists(SELECT 1
FROM users
WHERE id = :id);
-- :name update-user! :! :n -- :name update-user! :! :n
-- :doc updates an existing user record -- :doc updates an existing user record
@ -16,19 +26,19 @@ WHERE id = :id;
-- :doc updates user token and returns updated user -- :doc updates user token and returns updated user
UPDATE users UPDATE users
SET token = :token SET token = :token
WHERE login = :login WHERE id = :id
RETURNING id, login, name, email, token, address, created; RETURNING id, login, name, email, token, address, created;
-- :name update-user-address! :! :n -- :name update-user-address! :! :n
UPDATE users UPDATE users
SET address = :address SET address = :address
WHERE login = :login; WHERE id = :id;
-- :name get-user :? :1 -- :name get-user :? :1
-- :doc retrieve a user given the login. -- :doc retrieve a user given the login.
SELECT * SELECT *
FROM users FROM users
WHERE login = :login; WHERE id = :id;
-- Repositories -------------------------------------------------------------------- -- Repositories --------------------------------------------------------------------
@ -56,10 +66,68 @@ RETURNING repo_id, login, repo, enabled;
-- :doc returns enabled repositories for a given login -- :doc returns enabled repositories for a given login
SELECT repo_id SELECT repo_id
FROM repositories FROM repositories
WHERE login = :login AND enabled = TRUE; WHERE user_id = :user_id AND enabled = TRUE;
-- :name update-hook-id :! :n -- :name update-hook-id :! :n
-- :doc updates hook_id of a specified repository -- :doc updates hook_id of a specified repository
UPDATE repositories UPDATE repositories
SET hook_id = :hook_id SET hook_id = :hook_id
WHERE repo_id = :repo_id; WHERE repo_id = :repo_id;
-- Issues --------------------------------------------------------------------------
-- :name create-issue! :! :n
-- :doc creates issue
INSERT INTO issues (repo_id, issue_id, issue_number, address)
SELECT
:repo_id,
:issue_id,
:issue_number,
:address
WHERE NOT exists(SELECT 1
FROM issues
WHERE repo_id = :repo_id AND issue_id = :issue_id);
-- :name close-issue! :<! :1
-- :doc updates issue with commit id
UPDATE issues
SET commit_id = :commit_id
WHERE issue_id = :issue_id
RETURNING repo_id, issue_id, issue_number, address, commit_id;
-- Pull Requests -------------------------------------------------------------------
-- :name create-pull-request! :! :n
-- :doc creates pull request
INSERT INTO pull_requests (repo_id, pr_id, user_id, parents)
SELECT
:repo_id,
:pr_id,
:user_id,
:parents
WHERE NOT exists(SELECT 1
FROM pull_requests
WHERE repo_id = :repo_id AND pr_id = :pr_id);
-- Bounties ------------------------------------------------------------------------
-- :name bounties-list :? :*
-- :doc lists fixed issues
SELECT
i.address AS issue_address,
i.repo_id AS repo_id,
p.pr_id AS pr_id,
p.user_id AS user_id,
u.address AS payout_address,
u.login AS user_login,
u.name AS user_name,
r.repo AS repo_name
FROM issues i
INNER JOIN pull_requests p
ON p.parents LIKE '%' || i.commit_id || '%'
AND p.repo_id = i.repo_id
INNER JOIN users u
ON u.id = p.user_id
INNER JOIN repositories r
ON r.repo_id = i.repo_id
WHERE r.user_id = :owner_id;

View File

@ -22,11 +22,12 @@
var context = "{{servlet-context}}"; var context = "{{servlet-context}}";
var csrfToken = "{{csrf-token}}"; var csrfToken = "{{csrf-token}}";
var authorizeUrl = "{{authorize-url}}"; var authorizeUrl = "{{authorize-url}}";
var token = "{{token}}";
var userId = "{{userId}}";
var user = "{{login}}"; var user = "{{login}}";
if (user === "") { if (user === "") {
user = null; user = null;
} }
var token = "{{token}}";
</script> </script>
{% script "/js/app.js" %} {% script "/js/app.js" %}
</body> </body>

View File

@ -0,0 +1,19 @@
(ns commiteth.db.issues
(:require [commiteth.db.core :refer [*db*] :as db]
[clojure.java.jdbc :as jdbc]
[clojure.set :refer [rename-keys]]))
(defn create
"Creates issue"
[repo-id issue-id issue-number address]
(jdbc/with-db-connection [con-db *db*]
(db/create-issue! con-db {:repo_id repo-id
:issue_id issue-id
:issue_number issue-number
:address address})))
(defn close
"Updates issue with commit_id"
[issue-id commit-id]
(jdbc/with-db-connection [con-db *db*]
(db/close-issue! con-db {:issue_id issue-id :commit_id commit-id})))

View File

@ -0,0 +1,10 @@
(ns commiteth.db.pull-requests
(:require [commiteth.db.core :refer [*db*] :as db]
[clojure.java.jdbc :as jdbc]
[clojure.set :refer [rename-keys]]))
(defn create
"Creates pull-request"
[pull-request]
(jdbc/with-db-connection [con-db *db*]
(db/create-pull-request! con-db pull-request)))

View File

@ -19,10 +19,10 @@
(defn get-enabled (defn get-enabled
"Lists enabled repositories ids for a given login" "Lists enabled repositories ids for a given login"
[login] [user-id]
(->> (->>
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/get-enabled-repositories con-db {:login login})) (db/get-enabled-repositories con-db {:user_id user-id}))
(mapcat vals))) (mapcat vals)))
(defn update-hook-id (defn update-hook-id

View File

@ -1,13 +1,13 @@
(ns commiteth.db.users (ns commiteth.db.users
(:require [commiteth.db.core :refer [*db*] :as db] (:require [commiteth.db.core :refer [*db*] :as db]
[clojure.java.jdbc :as jdbc]) [clojure.java.jdbc :as jdbc])
(:import [java.util Date UUID])) (:import [java.util Date]))
(defn create-user (defn create-user
[login name email token] [user-id login name email token]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/create-user! con-db (db/create-user! con-db
{:id (str (UUID/randomUUID)) {:id user-id
:login login :login login
:name name :name name
:email email :email email
@ -16,22 +16,22 @@
:created (new Date)}))) :created (new Date)})))
(defn get-user (defn get-user
[login] [user-id]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/get-user con-db {:login login}))) (db/get-user con-db {:id user-id})))
(defn exists? (defn exists?
[login] [user-id]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(some? (db/get-user con-db {:login login})))) (some? (db/get-user con-db {:id user-id}))))
(defn update-user-address (defn update-user-address
[login address] [user-id address]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/update-user-address! con-db {:login login :address address}))) (db/update-user-address! con-db {:id user-id :address address})))
(defn update-user-token (defn update-user-token
"Updates user token and returns updated user" "Updates user token and returns updated user"
[login token] [user-id token]
(jdbc/with-db-connection [con-db *db*] (jdbc/with-db-connection [con-db *db*]
(db/update-user-token! con-db {:login login :token token}))) (db/update-user-token! con-db {:id user-id :token token})))

View File

@ -41,6 +41,9 @@
:client-id client-id :client-id client-id
:client-token client-secret}) :client-token client-secret})
(defn- self-auth-params []
{:auth (str self ":" self-password)})
(def repo-fields (def repo-fields
[:id [:id
:name :name
@ -85,11 +88,17 @@
(defn remove-webhook (defn remove-webhook
[token user repo hook-id] [token user repo hook-id]
(println "removing webhook") (println "removing webhook")
(println token user repo hook-id)
(repos/delete-hook user repo hook-id (auth-params token))) (repos/delete-hook user repo hook-id (auth-params token)))
(defn post-comment (defn post-comment
[user repo issue-id] [user repo issue-id]
(issues/create-comment user repo issue-id (issues/create-comment user repo issue-id
"a comment with an image link to the web service" "a comment with an image link to the web service" (self-auth-params)))
{:auth (str self ":" self-password)}))
(defn get-commit
[user repo commit-id]
(repos/specific-commit user repo commit-id (self-auth-params)))
(defn get-issue-events
[user repo issue-id]
(issues/issue-events user repo issue-id (self-auth-params)))

View File

@ -4,8 +4,8 @@
[ring.util.http-response :as response] [ring.util.http-response :as response]
[clojure.java.io :as io])) [clojure.java.io :as io]))
(defn home-page [{login :login token :token}] (defn home-page [{user-id :id login :login token :token}]
(layout/render "home.html" {:login login :token token})) (layout/render "home.html" {:userId user-id :login login :token token}))
(defroutes home-routes (defroutes home-routes
(GET "/" {{identity :identity} :session} (GET "/" {{identity :identity} :session}

View File

@ -13,12 +13,13 @@
(defn- get-or-create-user (defn- get-or-create-user
[token] [token]
(let [user (github/get-user token) (let [user (github/get-user token)
{email :email {email :email
name :name name :name
login :login} user] login :login
user-id :id} user]
(or (or
(users/update-user-token login token) (users/update-user-token user-id token)
(users/create-user login name email token)))) (users/create-user user-id login name email token))))
(defroutes redirect-routes (defroutes redirect-routes
(GET "/callback" [code state] (GET "/callback" [code state]

View File

@ -34,16 +34,16 @@
(context "/api" [] (context "/api" []
(POST "/user/address" [] (POST "/user/address" []
:auth-rules authenticated? :auth-rules authenticated?
:body-params [user :- String, address :- String] :body-params [user-id :- String, address :- String]
:summary "Update user address" :summary "Update user address"
(let [result (users/update-user-address user address)] (let [result (users/update-user-address user-id address)]
(if (= 1 result) (if (= 1 result)
(ok) (ok)
(internal-server-error)))) (internal-server-error))))
(GET "/user" [] (GET "/user" []
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
(ok {:user (users/get-user (:login user))})) (ok {:user (users/get-user (:id user))}))
(GET "/user/repositories" [] (GET "/user/repositories" []
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
@ -51,7 +51,7 @@
(GET "/repositories" [] (GET "/repositories" []
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user
(ok (repositories/get-enabled (:login user)))) (ok (repositories/get-enabled (:id user))))
(POST "/repository/toggle" {:keys [params]} (POST "/repository/toggle" {:keys [params]}
:auth-rules authenticated? :auth-rules authenticated?
:current-user user :current-user user

View File

@ -1,24 +1,95 @@
(ns commiteth.routes.webhooks (ns commiteth.routes.webhooks
(:require [compojure.core :refer [defroutes POST]] (:require [compojure.core :refer [defroutes POST]]
[commiteth.github.core :as github] [commiteth.github.core :as github]
[ring.util.http-response :refer [ok]])) [commiteth.db.pull-requests :as pull-requests]
[commiteth.db.issues :as issues]
[commiteth.db.users :as users]
[ring.util.http-response :refer [ok]]
[clojure.string :refer [join]])
(:import [java.util UUID]))
(def label-name "bounty") (def label-name "bounty")
(defn find-issue-closed-event
[events]
(first (filter #(= "closed" (:event %)) events)))
(defn handle-issue-closed
[{{{user :login} :owner repo :name} :repository
{issue-id :id issue-number :number} :issue}]
(future
(->>
(github/get-issue-events user repo issue-number)
(find-issue-closed-event)
(:commit_id)
(issues/close issue-id))))
(defn get-commit-parents
[commit]
(->> commit :parents (map :sha) (join ",")))
(defn handle-pull-request-closed
[{{{owner :login} :owner
repo-name :name
repo-id :id} :repository
{{user-id :id
login :login
name :name} :user
id :id
merge-commit-sha :merge_commit_sha} :pull_request}]
(future
(->>
(github/get-commit owner repo-name merge-commit-sha)
(get-commit-parents)
(hash-map :parents)
(merge {:repo_id repo-id
:pr_id id
:user_id user-id})
(pull-requests/create))
(users/create-user user-id login name nil nil)))
(defn labeled-as-bounty?
[action issue]
(and
(= "labeled" action)
(= label-name (get-in issue [:label :name]))))
(defn has-bounty-label?
[issue]
(let [labels (get-in issue [:issue :labels])]
(some #(= label-name (:name %)) labels)))
(defn gen-address []
(UUID/randomUUID))
(defn handle-issue (defn handle-issue
[issue] [issue]
(when-let [action (:action issue)] (when-let [action (:action issue)]
(when (and (when (labeled-as-bounty? action issue)
(= "labeled" action)
(= label-name (get-in issue [:label :name])))
(github/post-comment (github/post-comment
(get-in issue [:repository :owner :login]) (get-in issue [:repository :owner :login])
(get-in issue [:repository :name]) (get-in issue [:repository :name])
(get-in issue [:issue :number])))) (get-in issue [:issue :number]))
(let [repo-id (get-in issue [:repository :id])
issue (:issue issue)
issue-id (:id issue)
issue-number (:number issue)]
(issues/create repo-id issue-id issue-number (gen-address))))
(when (and
(= "closed" action)
(has-bounty-label? issue))
(handle-issue-closed issue)))
(ok (str issue))) (ok (str issue)))
(defn handle-pull-request
[pull-request]
(when (= "closed" (:action pull-request))
(handle-pull-request-closed pull-request))
(ok (str pull-request)))
(defroutes webhook-routes (defroutes webhook-routes
(POST "/webhook" {:keys [params headers]} (POST "/webhook" {:keys [params headers]}
(case (get headers "x-github-event") (case (get headers "x-github-event")
"issues" (handle-issue params) "issues" (handle-issue params)
"pull_request" (handle-pull-request params)
(ok)))) (ok))))

View File

@ -85,7 +85,7 @@
(defn load-user [] (defn load-user []
(when-let [login js/user] (when-let [login js/user]
(rf/dispatch [:set-active-user {:login login :token js/token}]))) (rf/dispatch [:set-active-user {:login login :id js/userId :token js/token}])))
(defn init! [] (defn init! []
(rf/dispatch-sync [:initialize-db]) (rf/dispatch-sync [:initialize-db])

View File

@ -84,9 +84,9 @@
(reg-event-fx (reg-event-fx
:save-user-address :save-user-address
(fn [{:keys [db]} [_ user address]] (fn [{:keys [db]} [_ user-id address]]
{:db db {:db db
:http {:method POST :http {:method POST
:url "/api/user/address" :url "/api/user/address"
:on-success #(println %) :on-success #(println %)
:params {:user user :address address}}})) :params {:user_id user-id :address address}}}))

View File

@ -5,13 +5,13 @@
[clojure.set :refer [rename-keys]])) [clojure.set :refer [rename-keys]]))
(defn save-address (defn save-address
[login address] [user-id address]
(fn [_] (fn [_]
(rf/dispatch [:save-user-address login address]))) (rf/dispatch [:save-user-address user-id address])))
(defn address-settings [] (defn address-settings []
(let [user (rf/subscribe [:user]) (let [user (rf/subscribe [:user])
login (:login @user) user-id (:id @user)
address (rf/subscribe [:get-in user-address-path])] address (rf/subscribe [:get-in user-address-path])]
(fn [] (fn []
[:div.form-group [:div.form-group
@ -19,7 +19,7 @@
[input {:placeholder "Address" [input {:placeholder "Address"
:value-path user-address-path}] :value-path user-address-path}]
[:button.btn.btn-primary.btn-lg [:button.btn.btn-primary.btn-lg
{:on-click (save-address login @address)} {:on-click (save-address user-id @address)}
"Save"]]))) "Save"]])))
(defn repository-row [repo] (defn repository-row [repo]