Skip to content

Commit

Permalink
Merge pull request #5757 from jwarwick/twitch
Browse files Browse the repository at this point in the history
API Access to Game State
  • Loading branch information
jwarwick authored Apr 27, 2021
2 parents efdf48d + da09454 commit e3d12fd
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 19 deletions.
3 changes: 2 additions & 1 deletion src/clj/game/core/set_up.clj
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@

(defn- init-game-state
"Initialises the game state"
[{:keys [players gameid timer spectatorhands save-replay room]}]
[{:keys [players gameid timer spectatorhands api-access save-replay room]}]
(let [corp (some #(when (corp? %) %) players)
runner (some #(when (runner? %) %) players)
corp-deck (create-deck (:deck corp))
Expand All @@ -101,6 +101,7 @@
(t/now)
{:timer timer
:spectatorhands spectatorhands
:api-access api-access
:save-replay save-replay}
(new-corp (:user corp) corp-identity corp-options (map #(assoc % :zone [:deck]) corp-deck) corp-deck-id corp-quote)
(new-runner (:user runner) runner-identity runner-options (map #(assoc % :zone [:deck]) runner-deck) runner-deck-id runner-quote)))))
Expand Down
3 changes: 2 additions & 1 deletion src/clj/tasks/index.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

(def ^:const indexes
(let [case-insensitive-index-opts {:collation {:locale "en" :strength (int 2)}}]
[["cards" (array-map :code 1)]
[["api-keys" (array-map :api-key 1)]
["cards" (array-map :code 1)]
["cards" (array-map :previous-versions 1)]
["cards" (array-map :type 1)]
["decks" (array-map :username 1)]
Expand Down
15 changes: 11 additions & 4 deletions src/clj/web/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
[web.admin :as admin]
[web.tournament :as tournament]
[web.decks :as decks]
[web.api-keys :as api-keys]
[web.game-api :as game-api]
[compojure.route :as route]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
Expand Down Expand Up @@ -54,7 +56,8 @@
(POST "/register" [] auth/register-handler)
(POST "/login" [] auth/login-handler)
(POST "/forgot" [] auth/forgot-password-handler)
(POST "/reset/:token" [] auth/reset-password-handler))
(POST "/reset/:token" [] auth/reset-password-handler)
(GET "/game/deck" [] game-api/deck-handler))

(defroutes admin-routes
(POST "/admin/announce" [] admin/announce-create-handler)
Expand Down Expand Up @@ -94,7 +97,11 @@
(GET "/data/decks" [] decks/decks-handler)
(POST "/data/decks" [] decks/decks-create-handler)
(PUT "/data/decks" [] decks/decks-save-handler)
(DELETE "/data/decks/:id" [] decks/decks-delete-handler))
(DELETE "/data/decks/:id" [] decks/decks-delete-handler)

(GET "/data/api-keys" [] api-keys/api-keys-handler)
(POST "/data/api-keys" [] api-keys/api-keys-create-handler)
(DELETE "/data/api-keys/:id" [] api-keys/api-keys-delete-handler))

(defroutes tournament-routes
(GET "/tournament-auth/:username" [] tournament/auth))
Expand All @@ -106,8 +113,8 @@

(defroutes routes
(wrap-routes private-routes wrap-anti-forgery)
(wrap-routes public-CSRF-routes wrap-anti-forgery)
public-routes)
public-routes
(wrap-routes public-CSRF-routes wrap-anti-forgery))

(defn wrap-return-favicon [handler]
(fn [req]
Expand Down
43 changes: 43 additions & 0 deletions src/clj/web/api_keys.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
(ns web.api-keys
(:require [web.mongodb :refer [object-id]]
[web.utils :refer [response]]
[web.ws :as ws]
[monger.collection :as mc]
[monger.result :refer [acknowledged?]])
(:import org.bson.types.ObjectId))

(defn api-keys-handler [{db :system/db
{username :username} :user}]
(if username
(response 200 (mc/find-maps db "api-keys" {:username username}))
(response 401 {:message "Unauthorized"})))

(defn api-keys-create-handler [{db :system/db
{username :username} :user}]
(if username
(let [new-key (java.util.UUID/randomUUID)
new-entry (mc/insert db "api-keys"
{:username username
:date (java.util.Date.)
:api-key new-key})]
(if (acknowledged? new-entry)
(if (acknowledged? (mc/update db "users" {:username username} {"$set" {:has-api-keys true}}))
(response 201 {:message "Created API Key"})
(response 500 {:message "Failed to update user info"}))
(response 500 {:message "Failed to create API Key"})))
(response 401 {:message "Unauthorized"})))

(defn api-keys-delete-handler [{db :system/db
{username :username} :user
{id :id} :params}]
(try
(if (and username id)
(if (acknowledged? (mc/remove db "api-keys" {:_id (object-id id) :username username}))
(let [key-count (mc/count db "api-keys" {:username username})]
(when-not (pos? key-count)
(mc/update db "users" {:username username} {"$set" {:has-api-keys false}}))
(response 200 {:message "Deleted"}))
(response 403 {:message "Forbidden"}))
(response 401 {:message "Unauthorized"}))
(catch Exception ex
(response 409 {:message "Unknown API Key"}))))
2 changes: 1 addition & 1 deletion src/clj/web/auth.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
(defn wrap-user [handler]
(fn [{db :system/db
:keys [cookies] :as req}]
(let [user-keys [:_id :username :emailhash :isadmin :ismoderator :tournament-organizer :special :options :stats]
(let [user-keys [:_id :username :emailhash :isadmin :ismoderator :tournament-organizer :special :options :stats :has-api-keys]
auth-cookie (get cookies "session")
user (when auth-cookie
(unsign-token (:value auth-cookie)))
Expand Down
49 changes: 49 additions & 0 deletions src/clj/web/game_api.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
(ns web.game-api
(:require [web.mongodb :refer [object-id]]
[web.decks :as decks]
[web.lobby :as lobby]
[web.utils :refer [response]]
[monger.collection :as mc]))

(defn- make-link [host path] (str host path))

(defn- make-card-info [host card]
{:title (:title (:card card))
:qty (:qty card)
:image (make-link host (get-in card [:card :images :en :default :stock]))})

(defn- get-deck-id [username game]
(if (:started game)
(let [state @(:state game)
side (if (= username (get-in state [:runner :user :username])) :runner :corp)]
(get-in state [side :deck-id]))
(:_id (:deck (first (filter #(= username (get-in % [:user :username])) (:players game)))))))

(defn- get-deck [db username game]
(if-let [deck-id (get-deck-id username game)]
(decks/update-deck (mc/find-one-as-map db "decks" {:_id (object-id deck-id) :username username}))
nil))

(defn deck-handler [{db :system/db
scheme :scheme
headers :headers}]
(if-let [api-key (get headers "x-jnet-api")]
(let [api-uuid (try
(java.util.UUID/fromString api-key)
(catch Exception e nil))
api-record (mc/find-one-as-map db "api-keys" {:api-key api-uuid} ["username"])
username (:username api-record)]
(if username
(let [game (lobby/game-for-username username)
allow-access (:api-access game)]
(if (and game allow-access)
(if-let [deck (get-deck db username game)]
(let [host (str (name scheme) "://" (get headers "host"))]
(response 200 {:name (:name deck)
:identity {:title (get-in deck [:identity :title])
:image (make-link host (get-in deck [:identity :images :en :default :stock]))}
:cards (map #(make-card-info host %) (:cards deck))}))
(response 204 {:message "No deck selected"}))
(response 403 {:message "No game for key or API Access not enabled"})))
(response 404 {:message "Unknown X-JNet-API key"})))
(response 400 {:message "No X-JNet-API header specified"})))
16 changes: 13 additions & 3 deletions src/clj/web/lobby.clj
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,27 @@
[client-id]
(get @all-games (get @client-gameids client-id)))

(defn- username-is-player
[username game]
(some #(= username (get-in % [:user :username])) (:players game)))

(defn game-for-username
"Returns the game map the given username is playing (but not spectating)"
[username]
(first (filter #(username-is-player username %) (vals @all-games))))

(defn lobby-clients
"Returns a seq of all client-ids playing or spectating a gameid."
[gameid]
(let [game (game-for-id gameid)]
(keep :ws-id (concat (:players game) (:spectators game)))))

(def lobby-only-keys [:messages :spectators :mute-spectators :spectatorhands :timer])

(let [public-lobby-updates (atom {})
game-lobby-updates (atom {})
send-ready (atom true)]

(def lobby-only-keys [:messages :spectators :mute-spectators :spectatorhands :timer :api-access])

(defn- game-public-view
"Strips private server information from a game map, preparing to send the game to clients."
[gameid game]
Expand Down Expand Up @@ -251,14 +260,15 @@
(defmethod ws/-msg-handler :lobby/create
[{{{:keys [username] :as user} :user} :ring-req
client-id :client-id
{:keys [title format timer allow-spectator save-replay
{:keys [title format timer allow-spectator save-replay api-access
spectatorhands password room side]} :?data}]
(let [gameid (java.util.UUID/randomUUID)
game {:date (java.util.Date.)
:gameid gameid
:title title
:allow-spectator allow-spectator
:save-replay save-replay
:api-access api-access
:spectatorhands spectatorhands
:mute-spectators false
:password (when (not-empty password) (bcrypt/encrypt password))
Expand Down
44 changes: 43 additions & 1 deletion src/cljs/nr/account.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[goog.dom :as gdom]
[nr.auth :refer [valid-email?] :as auth]
[nr.appstate :refer [app-state]]
[nr.ajax :refer [POST GET PUT]]
[nr.ajax :refer [POST GET PUT DELETE]]
[nr.avatar :refer [avatar]]
[nr.translations :refer [tr]]
[nr.utils :refer [non-game-toast set-scroll-top store-scroll-top]]
Expand Down Expand Up @@ -191,6 +191,44 @@
(reagent-modals/close-modal!))}
(tr [:settings.cancel "Cancel"])]]]])))

(defn- update-api-keys-response [response s]
(let [status (:status response)]
(if (or (= 200 status)
(= 201 status))
(do
(go (swap! s assoc :api-keys (:json (<! (GET "/data/api-keys")))))
(non-game-toast "Updated API Keys" "success" nil))
(non-game-toast "Failed to update API Keys" "error" nil))))

(defn- delete-api-key [id s]
(go (let [response (<! (DELETE (str "/data/api-keys/" id)))]
(update-api-keys-response response s))))

(defn- create-api-key [s]
(go (let [response (<! (POST "/data/api-keys" {} :json))]
(update-api-keys-response response s))))

(defn- api-keys [s]
(r/with-let [keys-cursor (r/cursor s [:api-keys])]
[:section
[:h3 (tr [:settings.api-keys "API Keys"])]
[:div.news-box.panel.blue-shade
[:ul.list
(doall
(for [d @keys-cursor]
[:li.news-item
{:key (:_id d)}
[:span
[:button.delete
{:on-click #(do (.preventDefault %)
(delete-api-key (:_id d) s))}
(tr [:settings.delete-api-key "Delete"])]]
[:span.date (-> (:date d) js/Date. js/moment (.format "DD-MMM-YYYY, HH:mm "))]
[:span.title (:api-key d "")]]))]]
[:button {:on-click #(do (.preventDefault %)
(create-api-key s))}
(tr [:settings.create-api-key "Create API Key"])]]))

(defn account-content [user s scroll-top]
(r/create-class
{
Expand Down Expand Up @@ -441,6 +479,8 @@
:on-click #(remove-user-from-block-list % s)} "X" ]
[:span.blocked-user-name (str " " bu)]]))]

[api-keys s]

[:section
[:button.float-right (tr [:settings.update-profile "Update Profile"])]
[:span.flash-message (:flash-message @s)]]]])}))
Expand Down Expand Up @@ -480,6 +520,8 @@
(when (= 200 (:status response))
(swap! state assoc :email (:email (:json response))))))

(go (swap! state assoc :api-keys (:json (<! (GET "/data/api-keys")))))

(fn []
(when (and @user (= "/account" (first @active)))
[:div.page-container
Expand Down
27 changes: 22 additions & 5 deletions src/cljs/nr/gamelobby.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
:editing true
:replay false
:save-replay (if (= "casual" (:room @s)) false true)
:api-access false
:flash-message ""
:protected false
:password ""
Expand Down Expand Up @@ -213,7 +214,7 @@
(swap! app-state dissoc :editing-game)
(ws/ws-send! [:lobby/create
(select-keys @s [:title :password :allow-spectator :save-replay
:spectatorhands :side :format :room :timer])])))))))
:spectatorhands :side :format :room :timer :api-access])])))))))

(defn leave-lobby [s]
(ws/ws-send! [:lobby/leave])
Expand Down Expand Up @@ -418,7 +419,7 @@
:room (:room @s)}])])

(defn create-new-game
[s]
[s user]
(when (:editing @s)
(if (:replay @s)
[:div
Expand Down Expand Up @@ -502,6 +503,7 @@
:value (:password @s)
:placeholder (tr [:lobby.password "Password"])
:maxLength "30"}]])

(when-not (= "casual" (:room @s))
[:p
[:label
Expand All @@ -518,6 +520,7 @@
:placeholder (tr [:lobby.timer-length "Timer length (minutes)"])}]])
[:div.infobox.blue-shade {:style {:display (if (:timed @s) "block" "none")}}
[:p "Timer is only for convenience: the game will not stop when timer runs out."]]

[:p
[:label
[:input {:type "checkbox" :checked (:save-replay @s)
Expand All @@ -528,7 +531,19 @@
" The file is available only after the game is finished."]
[:p "Only your latest 15 unshared games will be kept, so make sure to either download or share the match afterwards."]
[:p [:b "BETA Functionality:"] " Be aware that we might need to reset the saved replays, so " [:b "make sure to download games you want to keep."]
" Also, please keep in mind that we might need to do future changes to the site that might make replays incompatible."]]]]])))
" Also, please keep in mind that we might need to do future changes to the site that might make replays incompatible."]]

(let [has-keys (:has-api-keys @user false)]
[:p
[:label
[:input {:disabled (not has-keys)
:type "checkbox" :checked (:api-access @s)
:on-change #(swap! s assoc :api-access (.. % -target -checked))}]
(tr [:lobby.api-access "Allow API access to game information"])
(when (not has-keys)
(str " " (tr [:lobby.api-requires-key "(Requires an API Key in Settings)"])))]])
[:div.infobox.blue-shade {:style {:display (if (:api-access @s) "block" "none")}}
[:p "This allows access to information about your game to 3rd party extensions. Requires an API Key to be created in Settings"]]]]])))

(defn pending-game
[s decks games gameid password-gameid sets user]
Expand Down Expand Up @@ -597,7 +612,9 @@
" The file is available only after the game is finished."]
[:p "Only your latest 15 unshared games will be kept, so make sure to either download or share the match afterwards."]
[:p [:b "BETA Functionality:"] " Be aware that we might need to reset the saved replays, so " [:b "make sure to download games you want to keep."]
" Also, please keep in mind that we might need to do future changes to the site that might make replays incompatible."]])]
" Also, please keep in mind that we might need to do future changes to the site that might make replays incompatible."]])
(when (:api-access game)
[:li (tr [:lobby.api-access "Allow API access to game information"])])]

(when (:allow-spectator game)
[:div.spectators
Expand All @@ -612,7 +629,7 @@
(defn right-panel
[decks s games gameid password-gameid sets user]
[:div.game-panel
[create-new-game s]
[create-new-game s user]
[pending-game s decks games gameid password-gameid sets user]])

(defn game-lobby []
Expand Down
Loading

0 comments on commit e3d12fd

Please sign in to comment.