interactive suggestions

This commit is contained in:
Roman Volosovskyi 2017-03-23 18:52:38 +02:00 committed by Roman Volosovskyi
parent deb7b2b617
commit 03cab7ace2
23 changed files with 247 additions and 68 deletions

View File

@ -329,9 +329,10 @@ function jsSuggestions(params, context) {
])]); ])]);
if (suggestion.pressValue) { if (suggestion.pressValue) {
suggestionMarkup = status.components.touchable({ suggestionMarkup = status.components.touchable({
onPress: [status.events.SET_VALUE, suggestion.pressValue] onPress: status.components.dispatch([status.events.SET_VALUE, suggestion.pressValue])
}, },
suggestionMarkup); suggestionMarkup
);
} }
sugestionsMarkup.push(suggestionMarkup); sugestionsMarkup.push(suggestionMarkup);
} }
@ -422,7 +423,7 @@ function phoneSuggestions(params, context) {
suggestions = ph.map(function (phone) { suggestions = ph.map(function (phone) {
return status.components.touchable( return status.components.touchable(
{onPress: [status.events.SET_COMMAND_ARGUMENT, [0, phone.number]]}, {onPress: status.components.dispatch([status.events.SET_VALUE, phone.number])},
status.components.view(suggestionContainerStyle, status.components.view(suggestionContainerStyle,
[status.components.view(suggestionSubContainerStyle, [status.components.view(suggestionSubContainerStyle,
[ [
@ -484,7 +485,7 @@ var faucets = [
function faucetSuggestions(params) { function faucetSuggestions(params) {
var suggestions = faucets.map(function (entry) { var suggestions = faucets.map(function (entry) {
return status.components.touchable( return status.components.touchable(
{onPress: [status.events.SET_COMMAND_ARGUMENT, [0, entry.url]]}, {onPress: status.components.dispatch([status.events.SET_COMMAND_ARGUMENT, [0, entry.url]])},
status.components.view( status.components.view(
suggestionContainerStyle, suggestionContainerStyle,
[status.components.view( [status.components.view(
@ -559,7 +560,7 @@ status.command({
function debugSuggestions(params) { function debugSuggestions(params) {
var suggestions = ["On", "Off"].map(function (entry) { var suggestions = ["On", "Off"].map(function (entry) {
return status.components.touchable( return status.components.touchable(
{onPress: [status.events.SET_COMMAND_ARGUMENT, [0, entry]]}, {onPress: status.components.dispatch([status.events.SET_VALUE, entry])},
status.components.view( status.components.view(
suggestionContainerStyle, suggestionContainerStyle,
[status.components.view( [status.components.view(

View File

@ -26,7 +26,7 @@ status.defineSubscription(
function (params) { function (params) {
return round(params.value); return round(params.value);
} }
) );
function superSuggestion(params, context) { function superSuggestion(params, context) {
var balance = parseFloat(web3.fromWei(web3.eth.getBalance(context.from), "ether")); var balance = parseFloat(web3.fromWei(web3.eth.getBalance(context.from), "ether"));
@ -69,11 +69,11 @@ function superSuggestion(params, context) {
validationText: validationText validationText: validationText
}); });
status.setSuggestions(view); return {markup: view};
}; };
status.on("text-change", superSuggestion); status.addListener("on-message-input-change", superSuggestion);
status.on("message", function (params, context) { status.addListener("on-message-send", function (params, context) {
if (isNaN(params.message)) { if (isNaN(params.message)) {
status.sendMessage("Seems that you don't want to send money :("); status.sendMessage("Seems that you don't want to send money :(");
return; return;

View File

@ -47,7 +47,3 @@ status.command({
type: status.types.TEXT, type: status.types.TEXT,
placeholder: I18n.t('location_address') placeholder: I18n.t('location_address')
}); });
status.addListener("init", function (params, context) {
return {"text-message": "Hello, man!"};
});

View File

@ -32,6 +32,16 @@
"bot-url": "local://mailman-bot" "bot-url": "local://mailman-bot"
}, },
"demo-bot":
{
"name":
{
"en": "Demo bot"
},
"dapp?": true,
"bot-url": "local://demo-bot"
},
"browse": "browse":
{ {
"name": "name":

View File

@ -1,7 +1,8 @@
var _status_catalog = { var _status_catalog = {
commands: {}, commands: {},
responses: {}, responses: {},
functions: {} functions: {},
subscriptions: {}
}, },
status = {}; status = {};
@ -109,6 +110,10 @@ function view(options, elements) {
return ['view', options].concat(elements); return ['view', options].concat(elements);
} }
function slider(options) {
return ['slider', options];
}
function image(options) { function image(options) {
return ['image', options]; return ['image', options];
} }
@ -121,6 +126,14 @@ function scrollView(options, elements) {
return ['scroll-view', options].concat(elements); return ['scroll-view', options].concat(elements);
} }
function subscribe(path) {
return ['subscribe', path];
}
function dispatch(path) {
return ['dispatch', path];
}
function webView(url) { function webView(url) {
return ['web-view', { return ['web-view', {
source: { source: {
@ -178,16 +191,25 @@ var status = {
components: { components: {
view: view, view: view,
text: text, text: text,
slider: slider,
image: image, image: image,
touchable: touchable, touchable: touchable,
scrollView: scrollView, scrollView: scrollView,
webView: webView, webView: webView,
validationMessage: validationMessage, validationMessage: validationMessage,
bridgedWebView: bridgedWebView bridgedWebView: bridgedWebView,
subscribe: subscribe,
dispatch: dispatch
}, },
setSuggestions: function (view) { setSuggestions: function (view) {
addContext("suggestions", view); addContext("suggestions", view);
}, },
setDefaultDb: function (db) {
addContext("default-db", db);
},
updateDb: function (db) {
addContext("update-db", db)
},
sendMessage: function (text) { sendMessage: function (text) {
addContext("text-message", text); addContext("text-message", text);
}, },
@ -202,9 +224,25 @@ var status = {
} }
logMessages.push(message); logMessages.push(message);
addContext("log-messages", logMessages); addContext("log-messages", logMessages);
},
defineSubscription: function (name, subscriptions, handler) {
_status_catalog.subscriptions[name] = {
subscriptions: subscriptions,
handler: handler
};
} }
}; };
function calculateSubscription(parameters, context) {
var subscriptionConfig = _status_catalog.subscriptions[parameters.name];
if (!subscriptionConfig) {
return;
}
return subscriptionConfig.handler(parameters.subscriptions);
}
status.addListener("subscription", calculateSubscription);
console = (function (old) { console = (function (old) {
return { return {

View File

@ -0,0 +1,65 @@
(ns status-im.bots.handlers
(:require [re-frame.core :as re-frame]
[status-im.components.status :as status]
[status-im.utils.handlers :as u]))
(defn check-subscriptions
[{:keys [bot-db] :as db} [handler {:keys [path key bot]}]]
(let [path' (or path [key])
subscriptions (get-in db [:bot-subscriptions path'])
current-bot-db (get bot-db bot)]
(doseq [{:keys [bot subscriptions name]} subscriptions]
(let [subs-values (reduce (fn [res [sub-name sub-path]]
(assoc res sub-name (get-in current-bot-db sub-path)))
{} subscriptions)]
(status/call-function!
{:chat-id bot
:function :subscription
:parameters {:name name
:subscriptions subs-values}
:callback #(re-frame/dispatch
[::calculated-subscription {:bot bot
:path [name]
:result %}])})))))
(u/register-handler
:set-bot-db
(re-frame/after check-subscriptions)
(fn [db [_ {:keys [bot key value]}]]
(assoc-in db [:bot-db bot key] value)))
(u/register-handler
:set-in-bot-db
(re-frame/after check-subscriptions)
(fn [db [_ {:keys [bot path value]}]]
(assoc-in db (concat [:bot-db bot] path) value)))
(u/register-handler
:register-bot-subscription
(fn [db [_ {:keys [bot subscriptions] :as opts}]]
(reduce
(fn [db [sub-name sub-path]]
(let [sub-path' (if (coll? sub-path) sub-path [sub-path])
sub-path'' (mapv keyword sub-path')]
(update-in db [:bot-subscriptions sub-path''] conj
(assoc-in opts [:subscriptions sub-name] sub-path''))))
db
subscriptions)))
(u/register-handler
::calculated-subscription
(u/side-effect!
(fn [_ [_ {:keys [bot path]
{:keys [error result]} :result
:as data}]]
(when-not error
(let [returned (:returned result)
opts {:bot bot
:path path
:value returned}]
(re-frame/dispatch [:set-in-bot-db opts]))))))
(u/register-handler
:update-bot-db
(fn [app-db [_ {:keys [bot db]}]]
(update-in app-db [:bot-db bot] merge db)))

View File

@ -0,0 +1,15 @@
(ns status-im.bots.subs
(:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :as re-frame]))
(re-frame/register-sub
:bot-subscription
(fn [db [_ path]]
(let [chat-id (re-frame/subscribe [:get-current-chat-id])]
(reaction (get-in @db (concat [:bot-db @chat-id] path))))))
(re-frame/register-sub
:current-bot-db
(fn [db]
(let [chat-id (re-frame/subscribe [:get-current-chat-id])]
(reaction (get-in @db [:bot-db @chat-id])))))

View File

@ -101,7 +101,7 @@
suggestions (suggestions/get-command-suggestions db chat-text) suggestions (suggestions/get-command-suggestions db chat-text)
global-commands (suggestions/get-global-command-suggestions db chat-text) global-commands (suggestions/get-global-command-suggestions db chat-text)
{:keys [dapp?]} (get-in db [:contacts chat-id])] {:keys [dapp?]} (get-in db [:contacts chat-id])]
(when (and dapp? (every? empty? [requests suggestions global-commands])) (when (and dapp? (every? empty? [requests suggestions]))
(dispatch [::check-dapp-suggestions chat-id chat-text])) (dispatch [::check-dapp-suggestions chat-id chat-text]))
(-> db (-> db
(assoc-in [:chats chat-id :request-suggestions] requests) (assoc-in [:chats chat-id :request-suggestions] requests)
@ -276,13 +276,14 @@
(handlers/register-handler (handlers/register-handler
::check-dapp-suggestions ::check-dapp-suggestions
(handlers/side-effect! (handlers/side-effect!
(fn [db [_ chat-id text]] (fn [{:keys [current-account-id] :as db} [_ chat-id text]]
(let [data (get-in db [:local-storage chat-id])] (let [data (get-in db [:local-storage chat-id])]
(status/call-function! (status/call-function!
{:chat-id chat-id {:chat-id chat-id
:function :on-message-input-change :function :on-message-input-change
:parameters {:message text} :parameters {:message text}
:context {:data data}}))))) :context {:data data
:from current-account-id}})))))
(handlers/register-handler (handlers/register-handler
:clear-seq-arguments :clear-seq-arguments

View File

@ -3,7 +3,6 @@
[re-frame.core :refer [enrich after debug dispatch path]] [re-frame.core :refer [enrich after debug dispatch path]]
[status-im.data-store.messages :as messages] [status-im.data-store.messages :as messages]
[status-im.chat.utils :as cu] [status-im.chat.utils :as cu]
[status-im.commands.utils :refer [generate-hiccup]]
[status-im.utils.random :as random] [status-im.utils.random :as random]
[status-im.constants :refer [wallet-chat-id [status-im.constants :refer [wallet-chat-id
content-type-command content-type-command

View File

@ -190,22 +190,28 @@
(register-handler ::send-dapp-message (register-handler ::send-dapp-message
(u/side-effect! (u/side-effect!
(fn [db [_ chat-id {:keys [content]}]] (fn [{:keys [current-account-id] :as db} [_ chat-id {:keys [content]}]]
(let [data (get-in db [:local-storage chat-id])] (let [data (get-in db [:local-storage chat-id])]
(status/call-function! (status/call-function!
{:chat-id chat-id {:chat-id chat-id
:function :on-message-send :function :on-message-send
:parameters {:message content} :parameters {:message content}
:context {:data data}}))))) :context {:data data
:from current-account-id}})))))
(register-handler :received-bot-response (register-handler :received-bot-response
(u/side-effect! (u/side-effect!
(fn [_ [_ {:keys [chat-id] :as params} {:keys [result] :as data}]] (fn [_ [_ {:keys [chat-id] :as params} {:keys [result] :as data}]]
(let [{:keys [returned context]} result (let [{:keys [returned context]} result
{:keys [markup text-message]} returned {:keys [markup text-message]} returned
{:keys [log-messages]} context] {:keys [log-messages update-db default-db]} context]
(when update-db
(dispatch [:update-bot-db {:bot chat-id
:db update-db}]))
(when markup (when markup
(dispatch [:suggestions-handler (assoc params :result data)])) (dispatch [:suggestions-handler (assoc params
:result data
:default-db default-db)]))
(doseq [message log-messages] (doseq [message log-messages]
(let [{:keys [message type]} message] (let [{:keys [message type]} message]
(when (or (not= type "debug") js/goog.DEBUG) (when (or (not= type "debug") js/goog.DEBUG)

View File

@ -8,14 +8,16 @@
icon]] icon]]
[status-im.chat.views.input.animations.expandable :refer [expandable-view]] [status-im.chat.views.input.animations.expandable :refer [expandable-view]]
[status-im.chat.views.input.utils :as input-utils] [status-im.chat.views.input.utils :as input-utils]
[status-im.commands.utils :as command-utils]
[status-im.i18n :refer [label]] [status-im.i18n :refer [label]]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[clojure.string :as str])) [clojure.string :as str]))
(defview parameter-box-container [] (defview parameter-box-container []
[parameter-box [:chat-parameter-box]] [parameter-box [:chat-parameter-box]
bot-db [:current-bot-db]]
(when (:hiccup parameter-box) (when (:hiccup parameter-box)
(:hiccup parameter-box))) (command-utils/generate-hiccup (:hiccup parameter-box) bot-db)))
(defview parameter-box-view [] (defview parameter-box-view []
[show-parameter-box? [:show-parameter-box?]] [show-parameter-box? [:show-parameter-box?]]

View File

@ -35,6 +35,37 @@
:else nil))) :else nil)))
(defn suggestions-handler!
[{:keys [contacts chats] :as db} [{:keys [chat-id default-db command parameter-index result]}]]
(let [{:keys [markup]} (get-in result [:result :returned])
{:keys [dapp? dapp-url]} (get contacts chat-id)
path (if command
[:chats chat-id :parameter-boxes (:name command) parameter-index :hiccup]
[:chats chat-id :parameter-boxes :message :hiccup])]
(when-not (= (get-in db path) markup)
(dispatch [:set-in path markup])
(when default-db
(dispatch [:update-bot-db {:bot chat-id
:db default-db}])))))
(defn suggestions-events-handler!
[{:keys [current-chat-id bot-db] :as db} [[n & data :as ev] val]]
(log/debug "Suggestion event: " n (first data) val)
(let [{:keys [dapp?]} (get-in db [:contacts current-chat-id])]
(case (keyword n)
:set-command-argument (dispatch [:set-command-argument (first data)])
:set-value (dispatch [:set-chat-input-text (first data)])
:set (let [opts {:bot current-chat-id
:path (mapv keyword data)
:value val}]
(dispatch [:set-in-bot-db opts]))
:set-value-from-db
(let [path (keyword (first data))
value (str (get-in bot-db [current-chat-id path]))]
(dispatch [:set-chat-input-text value]))
;; todo show error?
nil)))
(defn print-error-message! [message] (defn print-error-message! [message]
(fn [_ params] (fn [_ params]
(when (:error (last params)) (when (:error (last params))
@ -48,25 +79,11 @@
(reg-handler (reg-handler
:suggestions-handler :suggestions-handler
[(after (print-error-message! "Error on param suggestions"))] [(after (print-error-message! "Error on param suggestions"))]
(fn [{:keys [contacts chats] :as db} [{:keys [chat-id command parameter-index result]}]] (handlers/side-effect! suggestions-handler!))
(let [{:keys [markup]} (get-in result [:result :returned])
{:keys [dapp? dapp-url]} (get contacts chat-id)
hiccup (generate-hiccup markup)
path (if command
[:chats chat-id :parameter-boxes (:name command) parameter-index]
[:chats chat-id :parameter-boxes :message])]
(assoc-in db path (when hiccup
{:hiccup hiccup})))))
(reg-handler (reg-handler
:suggestions-event! :suggestions-event!
(handlers/side-effect! (handlers/side-effect! suggestions-events-handler!))
(fn [{:keys [current-chat-id] :as db} [[n arg]]]
(let [{:keys [dapp?]} (get-in db [:contacts current-chat-id])]
(case (keyword n)
:set-command-argument (dispatch [:set-command-argument arg])
:set-value (dispatch [:set-chat-input-text arg])
nil)))))
(reg-handler :set-local-storage (reg-handler :set-local-storage
(fn [{:keys [current-chat-id] :as db} [{:keys [data] :as event}]] (fn [{:keys [current-chat-id] :as db} [{:keys [data] :as event}]]

View File

@ -108,7 +108,7 @@
(into {}))) (into {})))
(defn add-commands (defn add-commands
[db [id _ {:keys [commands responses]}]] [db [id _ {:keys [commands responses subscriptions]}]]
(let [account @(subscribe [:get-current-account]) (let [account @(subscribe [:get-current-account])
commands' (filter-forbidden-names account id commands) commands' (filter-forbidden-names account id commands)
global-command (:global commands') global-command (:global commands')
@ -121,7 +121,7 @@
:commands-loaded true :commands-loaded true
:commands (mark-as :command commands'') :commands (mark-as :command commands'')
:responses (mark-as :response responses') :responses (mark-as :response responses')
:global-command global-command) :subscriptions subscriptions)
global-command global-command
(update :global-commands assoc (keyword id) (update :global-commands assoc (keyword id)
@ -173,7 +173,14 @@
;;(after #(dispatch [:update-suggestions])) ;;(after #(dispatch [:update-suggestions]))
(after (fn [_ [id]] (after (fn [_ [id]]
(dispatch [:invoke-commands-loading-callbacks id]) (dispatch [:invoke-commands-loading-callbacks id])
(dispatch [:invoke-chat-loaded-callbacks id])))] (dispatch [:invoke-chat-loaded-callbacks id])))
(after (fn [{:keys [contacts]} [id]]
(let [subscriptions (get-in contacts [id :subscriptions])]
(doseq [[name opts] subscriptions]
(dispatch [:register-bot-subscription
(assoc opts :bot id
:name name)])))))]
add-commands) add-commands)
(reg-handler ::loading-failed! (u/side-effect! loading-failed!)) (reg-handler ::loading-failed! (u/side-effect! loading-failed!))

View File

@ -4,6 +4,7 @@
[status-im.components.react :refer [text [status-im.components.react :refer [text
scroll-view scroll-view
view view
slider
web-view web-view
image image
touchable-highlight]] touchable-highlight]]
@ -20,6 +21,7 @@
(def elements (def elements
{:text text {:text text
:view view :view view
:slider slider
:scroll-view scroll-view :scroll-view scroll-view
:web-view web-view :web-view web-view
:image image :image image
@ -30,26 +32,36 @@
(defn get-element [n] (defn get-element [n]
(elements (keyword (.toLowerCase n)))) (elements (keyword (.toLowerCase n))))
(def events #{:onPress}) (def events #{:onPress :onValueChange :onSlidingComplete})
(defn wrap-event [event] (defn wrap-event [[_ event]]
#(dispatch [:suggestions-event! event])) (let [data (gensym)]
#(dispatch [:suggestions-event! (update event 0 keyword) %])))
(defn check-events [m] (defn check-events [m]
(let [ks (set (keys m)) (let [ks (set (keys m))
evs (set/intersection ks events)] evs (set/intersection ks events)]
(reduce #(update %1 %2 wrap-event) m evs))) (reduce #(update %1 %2 wrap-event) m evs)))
(defn generate-hiccup [markup] (defn generate-hiccup
;; todo implement validation ([markup]
(w/prewalk (generate-hiccup markup {}))
(fn [el] ([markup data]
(if (and (vector? el) (string? (first el))) (w/prewalk
(-> el (fn [el]
(update 0 get-element) (cond
(update 1 check-events))
el)) (and (vector? el) (= "subscribe" (first el)))
markup)) (let [path (mapv keyword (second el))]
(get-in data path))
(and (vector? el) (string? (first el)))
(-> el
(update 0 get-element)
(update 1 check-events))
:esle el))
markup)))
(defn reg-handler (defn reg-handler
([name handler] (reg-handler name nil handler)) ([name handler] (reg-handler name nil handler))

View File

@ -51,6 +51,7 @@
(def keyboard (.-Keyboard react-native)) (def keyboard (.-Keyboard react-native))
(def linking (.-Linking js/ReactNative)) (def linking (.-Linking js/ReactNative))
(def slider (get-class "Slider"))
;; Accessor methods for React Components ;; Accessor methods for React Components
(defn add-font-style [style-key {:keys [font] :as opts :or {font :default}}] (defn add-font-style [style-key {:keys [font] :as opts :or {font :default}}]

View File

@ -147,14 +147,14 @@
(.callJail status chat-id (cljs->json path) (cljs->json params') cb)))))) (.callJail status chat-id (cljs->json path) (cljs->json params') cb))))))
(defn call-function! (defn call-function!
[{:keys [chat-id function] :as opts}] [{:keys [chat-id function callback] :as opts}]
(let [path [:functions function] (let [path [:functions function]
params (select-keys opts [:parameters :context])] params (select-keys opts [:parameters :context])]
(call-jail (call-jail
chat-id chat-id
path path
params params
#(dispatch [:received-bot-response {:chat-id chat-id} %])))) (or callback #(dispatch [:received-bot-response {:chat-id chat-id} %])))))
(defn set-soft-input-mode [mode] (defn set-soft-input-mode [mode]
(when status (when status

View File

@ -3,7 +3,6 @@
[clojure.string :refer [join split]] [clojure.string :refer [join split]]
[status-im.utils.random :refer [timestamp]] [status-im.utils.random :refer [timestamp]]
[clojure.walk :refer [stringify-keys keywordize-keys]] [clojure.walk :refer [stringify-keys keywordize-keys]]
[status-im.commands.utils :refer [generate-hiccup]]
[cljs.reader :refer [read-string]] [cljs.reader :refer [read-string]]
[status-im.constants :as c]) [status-im.constants :as c])
(:refer-clojure :exclude [update])) (:refer-clojure :exclude [update]))

View File

@ -23,6 +23,7 @@
status-im.transactions.handlers status-im.transactions.handlers
status-im.network.handlers status-im.network.handlers
status-im.debug.handlers status-im.debug.handlers
status-im.bots.handlers
[status-im.utils.types :as t] [status-im.utils.types :as t]
[status-im.i18n :refer [label]] [status-im.i18n :refer [label]]
[status-im.constants :refer [console-chat-id]] [status-im.constants :refer [console-chat-id]]

View File

@ -8,7 +8,8 @@
status-im.contacts.subs status-im.contacts.subs
status-im.new-group.subs status-im.new-group.subs
status-im.participants.subs status-im.participants.subs
status-im.transactions.subs)) status-im.transactions.subs
status-im.bots.subs))
(register-sub :get (register-sub :get
(fn [db [_ k]] (fn [db [_ k]]

View File

@ -180,8 +180,9 @@
{:keys [hash]} (get transactions id) {:keys [hash]} (get transactions id)
pending-message (get transaction-subscribers message-id)] pending-message (get transaction-subscribers message-id)]
(when (and pending-message id hash) (when (and pending-message id hash)
(dispatch [::send-pending-message message-id hash]) (dispatch [::send-pending-message message-id hash]))
(dispatch [::remove-transaction id])))))) ;; todo revisit this
(dispatch [::remove-transaction id])))))
(def wrong-password-code "2") (def wrong-password-code "2")
(def discard-code "4") (def discard-code "4")

View File

@ -17,15 +17,18 @@
(def browse-js (slurp-bot :browse)) (def browse-js (slurp-bot :browse))
(def mailman-js (slurp-bot :mailman )) (def mailman-js (slurp-bot :mailman))
(def demo-bot-js (slurp-bot :demo_bot))
(def commands-js wallet-js) (def commands-js wallet-js)
(def resources (def resources
{:wallet-bot wallet-js {:wallet-bot wallet-js
:console-bot console-js :console-bot console-js
:browse-bot browse-js :browse-bot browse-js
:mailman-bot mailman-js}) :mailman-bot mailman-js
:demo-bot demo-bot-js})
(defn get-resource [url] (defn get-resource [url]
(let [resource-name (keyword (subs url (count local-protocol)))] (let [resource-name (keyword (subs url (count local-protocol)))]

View File

@ -7,5 +7,9 @@
(defmacro slurp-bot [bot-name & files] (defmacro slurp-bot [bot-name & files]
(->> (concat files ["translations.js" "bot.js"]) (->> (concat files ["translations.js" "bot.js"])
(map #(clojure.core/slurp (s/join "/" ["bots" (name bot-name) %]))) (map (fn [file-name]
(try
(clojure.core/slurp
(s/join "/" ["bots" (name bot-name) file-name]))
(catch Exception _ ""))))
(apply str))) (apply str)))